diff --git a/.coveragerc b/.coveragerc deleted file mode 100755 index df53bfe..0000000 --- a/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[run] -source = - exonetapi -omit = - tests/* diff --git a/.editorconfig b/.editorconfig index aa2aa13..c5f3a4a 100755 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,13 @@ -# http://editorconfig.org root = true [*] -indent_style = space -indent_size = 4 -end_of_line = lf charset = utf-8 -trim_trailing_whitespace = true +end_of_line = lf +indent_size = 4 +indent_style = space insert_final_newline = true +max_line_length = 88 +trim_trailing_whitespace = true -[*.yaml] +[*.{yaml,toml}] indent_size = 2 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..5c012b4 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203 +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist,.venv diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5ee4a99..2eb3659 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,6 +2,12 @@ version: 2 updates: - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + time: "03:00" + + - package-ecosystem: "pip" directory: "/" schedule: interval: daily diff --git a/.github/labels.yml b/.github/labels.yml index b7beb1e..544a10a 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -50,3 +50,7 @@ - name: "minor" color: 0e8a16 description: "This PR causes a minor version bump in the version number." + +- name: "skip-changelog" + color: fef2c0 + description: "This issue or PR is exempted from the release notes." diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 27e7076..d41411b 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,9 +1,12 @@ --- -name-template: "$RESOLVED_VERSION" -tag-template: "$RESOLVED_VERSION" -change-template: "- #$NUMBER $TITLE @$AUTHOR" +name-template: '$RESOLVED_VERSION' +tag-template: '$RESOLVED_VERSION' +change-template: '- [#$NUMBER] $TITLE [@$AUTHOR](https://github.com/$AUTHOR)' sort-direction: ascending +exclude-labels: + - 'skip-changelog' + categories: - title: "🚨 Breaking changes" labels: @@ -28,6 +31,7 @@ categories: labels: - "documentation" - title: "⬆️ Dependency updates" + collapse-after: 1 labels: - "dependencies" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 340ca9e..975607d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,39 +1,120 @@ --- name: CI -# yamllint disable-line rule:truthy on: push: branches: - master pull_request: - branches: - - master - release: - types: [released] jobs: - run-tests: - name: 📦 Build & publish package + lint: + name: Lint and validate code runs-on: ubuntu-latest steps: - - name: 📥 Checkout repository + - name: Checkout repository uses: actions/checkout@v3 - - name: 🛠 Set up Python all python version + - name: Set up Python 3.9 uses: actions/setup-python@v3 + id: setup-python with: python-version: 3.9 - - name: 📦 Install build, check-wheel-contents, and twine - run: python -m pip install build + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached .venv + id: cached-poetry-dependencies + uses: actions/cache@v2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: ${{ steps.cached-poetry-dependencies.outputs.cache-hit != 'true' }} + run: poetry install --no-interaction --no-root + + - name: Run flake8 + run: poetry run flake8 + + - name: Run bandit + run: poetry run bandit -r exonetapi + + test: + name: Run tests on python ${{ matrix.python-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10'] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 - - name: 🔨 Build package - run: python -m build --sdist --wheel . + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + id: setup-python + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true - - name: 🌏 Publish distribution to PyPI - if: ${{ github.event_name == 'release' }} - uses: pypa/gh-action-pypi-publish@release/v1 + - name: Load cached .venv + id: cached-poetry-dependencies + uses: actions/cache@v2 with: - password: ${{ secrets.PYPI_API_TOKEN }} + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: ${{ steps.cached-poetry-dependencies.outputs.cache-hit != 'true' }} + run: poetry install --no-interaction --no-root + + - name: Run pytest + run: poetry run pytest --cov exonetapi + + build: + name: Build package + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python 3.9 + uses: actions/setup-python@v3 + id: setup-python + with: + python-version: 3.9 + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached .venv + id: cached-poetry-dependencies + uses: actions/cache@v2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: ${{ steps.cached-poetry-dependencies.outputs.cache-hit != 'true' }} + run: poetry install --no-interaction --no-root + + - name: Build package + run: poetry build diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml deleted file mode 100644 index 9b74c78..0000000 --- a/.github/workflows/codeql.yaml +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: CodeQL - -# yamllint disable-line rule:truthy -on: - push: - branches: master - pull_request: - branches: master - schedule: - - cron: "30 1 * * 0" - -jobs: - codeql: - name: Scanning - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: ["python"] - - steps: - - name: 📥 Checkout repository - uses: actions/checkout@v3 - - - name: 🏗 Initialize CodeQL - uses: github/codeql-action/init@v1 - - - name: 🚀 Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml index 355e3ae..fc5c12e 100644 --- a/.github/workflows/draft-release.yaml +++ b/.github/workflows/draft-release.yaml @@ -1,18 +1,18 @@ --- -name: Release Drafter +name: Draft Release -# yamllint disable-line rule:truthy on: push: branches: - master jobs: - update_release_draft: - name: ✏️ Draft release + update-release-draft: + name: Update runs-on: ubuntu-latest + steps: - - name: 🚀 Run Release Drafter + - name: Run release drafter uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..4f8e0b0 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,38 @@ +--- +name: Publish + +on: + release: + types: [released] + +jobs: + publish: + name: Build & publish package + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python 3.9 + uses: actions/setup-python@v3 + with: + python-version: 3.9 + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Install dependencies + run: poetry install --no-interaction --no-root + + - name: Build package + run: poetry build + + - name: Publish distribution to PyPI + if: ${{ github.event_name == 'release' }} + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/labels.yaml b/.github/workflows/sync-labels.yaml similarity index 56% rename from .github/workflows/labels.yaml rename to .github/workflows/sync-labels.yaml index 3bd17ae..cf8c368 100644 --- a/.github/workflows/labels.yaml +++ b/.github/workflows/sync-labels.yaml @@ -1,7 +1,6 @@ --- -name: Sync Labels +name: Sync labels -# yamllint disable-line rule:truthy on: workflow_dispatch: push: @@ -12,13 +11,14 @@ on: jobs: labels: - name: ♻️ Sync labels + name: Sync labels runs-on: ubuntu-latest + steps: - - name: 📥 Checkout repository + - name: Checkout repository uses: actions/checkout@v3 - - name: 🚀 Run Label Syncer - uses: micnncim/action-label-syncer@v1 + - name: Run Label Syncer + uses: micnncim/action-label-syncer@v1.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index 62cc4de..0000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: Tests - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - run-tests: - name: 🪲 Run tests - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: [3.6, 3.7, 3.8, 3.9] - - steps: - - name: 📥 Checkout repository - uses: actions/checkout@v3 - - - name: 🛠 Set up Python all python version - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - architecture: x64 - - - name: 📦 Install dependencies - run: pip install -r requirements.txt - - - name: 🧰 Run unit tests - run: python -m unittest discover tests diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/verify-pr-labels.yaml similarity index 67% rename from .github/workflows/pr-labels.yaml rename to .github/workflows/verify-pr-labels.yaml index 187d545..1a804fa 100644 --- a/.github/workflows/pr-labels.yaml +++ b/.github/workflows/verify-pr-labels.yaml @@ -1,22 +1,23 @@ --- -name: PR +name: PR Labels -# yamllint disable-line rule:truthy on: pull_request_target: types: [opened, labeled, unlabeled, synchronize] jobs: - verify_pr_labels: - name: 🏷 Verify Labels + pr-labels: + name: Verify runs-on: ubuntu-latest + steps: - - name: ✅ Verify PR has a valid label + - name: Verify PR has a valid label uses: jesusvasquez333/verify-pr-label-action@v1.4.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" pull-request-number: "${{ github.event.pull_request.number }}" valid-labels: >- - breaking-change, bugfix, documentation, enhancement, - refactor, performance, new-feature, maintenance, ci, dependencies + breaking-change, bugfix, hotfix, documentation, enhancement, + refactor, performance, new-feature, maintenance, ci, + dependencies, skip-changelog disable-reviews: true diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index bc5c89d..061670c --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc .idea/* +.vscode/* *.egg-info/* dist/* diff --git a/LICENSE.md b/LICENSE.md old mode 100755 new mode 100644 index 3e0b21d..c6ee3f3 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License (MIT) -Copyright (c) 2018 Exonet +Copyright (c) 2022 Exonet > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst old mode 100755 new mode 100644 index f388479..b390f3e --- a/README.rst +++ b/README.rst @@ -5,8 +5,6 @@ Python 3 library for the Exonet API. .. image:: https://img.shields.io/pypi/v/exonetapi.svg?style=flat-square .. image:: https://img.shields.io/pypi/pyversions/exonetapi.svg?style=flat-square .. image:: https://img.shields.io/pypi/l/exonetapi.svg?style=flat-square -.. image:: https://img.shields.io/lgtm/grade/python/g/exonet/exonet-api-python.svg - :target: https://lgtm.com/projects/g/exonet/exonet-api-python/context:python Conventions ----------- @@ -54,9 +52,9 @@ This should make two API calls and print the ticket and email details for one of Testing ------- -Run unit tests and coverage:: +Run unit tests:: - coverage run -m unittest discover tests -v && coverage html + poetry run pytest --cov=exonetapi Change log ---------- @@ -66,7 +64,11 @@ Please see `releases ` for Security -------- -If you discover any security related issues please email `support@exonet.nl `_ instead of using the issue tracker. +Run bandit:: + + poetry run bandit -r exonetapi + +If you discover any security related issues please email `development@exonet.nl `_ instead of using the issue tracker. Credits ------- diff --git a/examples/dns_zone_details.py b/examples/dns_zone_details.py index 39beb00..881319f 100644 --- a/examples/dns_zone_details.py +++ b/examples/dns_zone_details.py @@ -8,36 +8,41 @@ # Authorize with a personal access token. client.authenticator.set_token(sys.argv[1]) -''' -Get a single dns_zone resource. Because depending on who is authorized, the dns_zone IDs change, all -dns_zones are retrieved with a limit of 1. From this result, the first DNS zone is used. In a real -world scenario you would call something like -`zone = client.resource('dns_zones').get('VX09kwR3KxNo')` to get a single DNS zone by it's ID. -''' -zones = client.resource('dns_zones').size(1).get() +""" +Get a single dns_zone resource. Because depending on who is authorized, the dns_zone +IDs change, all dns_zones are retrieved with a limit of 1. From this result, the first +DNS zone is used. In a real world scenario you would call something like +`zone = client.resource('dns_zones').get('VX09kwR3KxNo')` to get a single DNS zone by +it's ID. +""" +zones = client.resource("dns_zones").size(1).get() # Show error when there are no zones available. if len(zones) == 0: - print('There are no DNS zones found.') + print("There are no DNS zones found.") sys.exit() zone = zones[0] # Output DNS zone details. -print('DNS zone:\t{zone_name}'.format( - zone_name=zone.attribute('name'), -)) +print( + "DNS zone:\t{zone_name}".format( + zone_name=zone.attribute("name"), + ) +) # Get the records for this zone. -records = zone.related('records').get() +records = zone.related("records").get() # Show records. for record in records: - print('{type}\t{fqdn}\t{ttl}\t{content}'.format( - type=record.attribute('type'), - fqdn=record.attribute('fqdn'), - ttl=record.attribute('ttl'), - content=record.attribute('content') - )) - -print('\n') + print( + "{type}\t{fqdn}\t{ttl}\t{content}".format( + type=record.attribute("type"), + fqdn=record.attribute("fqdn"), + ttl=record.attribute("ttl"), + content=record.attribute("content"), + ) + ) + +print("\n") diff --git a/examples/dns_zones.py b/examples/dns_zones.py index 2fb0596..8b645bd 100644 --- a/examples/dns_zones.py +++ b/examples/dns_zones.py @@ -8,16 +8,18 @@ # Authorize with a personal access token. client.authenticator.set_token(sys.argv[1]) -print('\nDNS zones (max 20):\n') +print("\nDNS zones (max 20):\n") # Get max 20 DNS zones. -zones = client.resource('dns_zones').size(20).get() +zones = client.resource("dns_zones").size(20).get() # Loop zones. for zone in zones: # Print zone name and record count. - print('{zone_name} - {record_count} records'.format( - zone_name=zone.attribute('name'), - record_count=len(zone.relationship('records')) - )) + print( + "{zone_name} - {record_count} records".format( + zone_name=zone.attribute("name"), + record_count=len(zone.relationship("records")), + ) + ) -print('\n') +print("\n") diff --git a/examples/new_dns_zones.py b/examples/new_dns_zones.py index e9271a9..35cd246 100644 --- a/examples/new_dns_zones.py +++ b/examples/new_dns_zones.py @@ -10,42 +10,42 @@ # Authorize with a personal access token. client.authenticator.set_token(sys.argv[1]) -print('\nAdd a new DNS zone\n') +print("\nAdd a new DNS zone\n") # Create a new resource with the desired attributes. -zone = ApiResource('dns_zones') -zone.attribute('name', 'my-new-zone.com') -zone.attribute('dnssec', True) +zone = ApiResource("dns_zones") +zone.attribute("name", "my-new-zone.com") +zone.attribute("dnssec", True) # Replace the customer ID with your own customer ID. -zone.relationship('customer', ApiResourceIdentifier('customers', 'X09kwRdbbAxN')) +zone.relationship("customer", ApiResourceIdentifier("customers", "X09kwRdbbAxN")) # Send the new resource to the API. zone_result = zone.post() # Add some records. -record1 = ApiResource('dns_records') -record1.attribute('name', 'www') -record1.attribute('type', 'A') -record1.attribute('content', '192.168.1.100') -record1.attribute('ttl', 3600) -record1.relationship('zone', ApiResourceIdentifier('dns_zones', zone_result.id())) +record1 = ApiResource("dns_records") +record1.attribute("name", "www") +record1.attribute("type", "A") +record1.attribute("content", "192.168.1.100") +record1.attribute("ttl", 3600) +record1.relationship("zone", ApiResourceIdentifier("dns_zones", zone_result.id())) record_result_1 = record1.post() -record2 = ApiResource('dns_records') -record2.attribute('name', 'test') -record2.attribute('type', 'A') -record2.attribute('content', '192.168.1.200') -record2.attribute('ttl', 3600) -record2.relationship('zone', ApiResourceIdentifier('dns_zones', zone_result.id())) +record2 = ApiResource("dns_records") +record2.attribute("name", "test") +record2.attribute("type", "A") +record2.attribute("content", "192.168.1.200") +record2.attribute("ttl", 3600) +record2.relationship("zone", ApiResourceIdentifier("dns_zones", zone_result.id())) record_result_2 = record2.post() # Change the the 'www' record to an AAAA record. -record_result_1.attribute('type', 'AAAA') -record_result_1.attribute('content', 'fe80::1ff:fe23:4567:890a') +record_result_1.attribute("type", "AAAA") +record_result_1.attribute("content", "fe80::1ff:fe23:4567:890a") record_result_1.patch() # Delete the test record. record_result_2.delete() -print('DNS Zone ID: {}'.format(zone_result.id())) -print('DNS Record ID: {}'.format(record_result_1.id())) -print(record_result_1.related('zone').get().attribute('name')) +print("DNS Zone ID: {}".format(zone_result.id())) +print("DNS Record ID: {}".format(record_result_1.id())) +print(record_result_1.related("zone").get().attribute("name")) diff --git a/examples/ticket_details.py b/examples/ticket_details.py index eae6192..7e115b4 100644 --- a/examples/ticket_details.py +++ b/examples/ticket_details.py @@ -8,49 +8,48 @@ # Authorize with a personal access token. client.authenticator.set_token(sys.argv[1]) -''' -Get a single ticket resource. Because depending on who is authorized, the ticket IDs change, all -tickets are retrieved with a limit of 1. From this result, the first ticket is used. In a real world -scenario you would call something like `ticket = client.resource('tickets').get('VX09kwR3KxNo')` to -get a single ticket by it's ID. -''' -tickets = client.resource('tickets').size(1).get() +""" +Get a single ticket resource. Because depending on who is authorized, the ticket IDs +change, all tickets are retrieved with a limit of 1. From this result, the first +ticket is used. In a real world scenario you would call something like +`ticket = client.resource('tickets').get('VX09kwR3KxNo')` to get a single ticket by +it's ID. +""" +tickets = client.resource("tickets").size(1).get() # Show this message when there are no tickets available. if len(tickets) == 0: - print('There are no tickets available') + print("There are no tickets available") sys.exit() ticket = tickets[0] print( - '\nTicket id:\t\t{id}\n' - 'Ticket subject:\t\t{subject}\n' - 'Created at:\t\t{ticket_date}\n' - 'Last message date:\t{last_message_date}\n'.format( - subject=ticket.attribute('last_message_subject'), - ticket_date=ticket.attribute('created_date'), - last_message_date=ticket.attribute('last_message_date'), - id=ticket.id() + "\nTicket id:\t\t{id}\n" + "Ticket subject:\t\t{subject}\n" + "Created at:\t\t{ticket_date}\n" + "Last message date:\t{last_message_date}\n".format( + subject=ticket.attribute("last_message_subject"), + ticket_date=ticket.attribute("created_date"), + last_message_date=ticket.attribute("last_message_date"), + id=ticket.id(), ) ) # Get the emails in the ticket. -emails = ticket.related('emails').get() +emails = ticket.related("emails").get() -print('This ticket has {mailCount} emails'.format( - mailCount=len(emails) -)) +print("This ticket has {mailCount} emails".format(mailCount=len(emails))) for email in emails: print( - '---------------------------------------------\n' - 'Email id:\t{id}\n' - 'Subject:\t{subject}\n' - 'Date:\t\t{message_date}\n\n' - '{message}'.format( - subject=email.attribute('subject'), - message_date=email.attribute('created_date'), + "---------------------------------------------\n" + "Email id:\t{id}\n" + "Subject:\t{subject}\n" + "Date:\t\t{message_date}\n\n" + "{message}".format( + subject=email.attribute("subject"), + message_date=email.attribute("created_date"), id=email.id(), - message=email.attribute('body')[:100] + message=email.attribute("body")[:100], ) ) diff --git a/examples/tickets.py b/examples/tickets.py index c00b022..1929325 100644 --- a/examples/tickets.py +++ b/examples/tickets.py @@ -8,23 +8,25 @@ # Authorize with a personal access token. client.authenticator.set_token(sys.argv[1]) -print('\nID and subject of all tickets (limit 5):\n') +print("\nID and subject of all tickets (limit 5):\n") # Get 5 tickets. -tickets = client.resource('tickets').size(5).get() +tickets = client.resource("tickets").size(5).get() for ticket in tickets: # For all the tickets, print the ticket ID with the subject. - print('{id} - {subject}'.format( - id=ticket.id(), - subject=ticket.attribute('last_message_subject') - )) + print( + "{id} - {subject}".format( + id=ticket.id(), subject=ticket.attribute("last_message_subject") + ) + ) -print('\nID and subject of all open tickets:\n') +print("\nID and subject of all open tickets:\n") # Get all tickets that are considered 'open' by Exonet. -open_tickets = client.resource('tickets').filter('open', 1).get() +open_tickets = client.resource("tickets").filter("open", 1).get() for ticket in open_tickets: # For the open tickets, print the ticket ID with the subject. - print('{id} - {subject}'.format( - id=ticket.id(), - subject=ticket.attribute('last_message_subject') - )) + print( + "{id} - {subject}".format( + id=ticket.id(), subject=ticket.attribute("last_message_subject") + ) + ) diff --git a/exonetapi/Client.py b/exonetapi/Client.py old mode 100755 new mode 100644 index 56912f7..56e0209 --- a/exonetapi/Client.py +++ b/exonetapi/Client.py @@ -22,15 +22,17 @@ class Client(metaclass=Singleton): """ # The URL to use for authentication. - authentication_endpoint = '/oauth/token' + authentication_endpoint = "/oauth/token" def __init__(self, host=None): - self.__host = 'https://api.exonet.nl' + self.__host = "https://api.exonet.nl" if host: self.set_host(host) - self.authenticator = Authenticator(self.get_host(), self.authentication_endpoint) + self.authenticator = Authenticator( + self.get_host(), self.authentication_endpoint + ) def set_host(self, host): """ @@ -43,8 +45,8 @@ def set_host(self, host): parsed_host = urlparse(host) # Make sure the host uses a valid scheme. - if parsed_host.scheme not in ('http', 'https'): - raise ConnectionAbortedError('Invalid protocol for host: %s' % host) + if parsed_host.scheme not in ("http", "https"): + raise ConnectionAbortedError("Invalid protocol for host: %s" % host) self.__host = parsed_host.geturl() diff --git a/exonetapi/RequestBuilder.py b/exonetapi/RequestBuilder.py old mode 100755 new mode 100644 index 6399d75..181e2d7 --- a/exonetapi/RequestBuilder.py +++ b/exonetapi/RequestBuilder.py @@ -9,12 +9,11 @@ class RequestBuilder(object): - """Create and make requests to the API. - """ + """Create and make requests to the API.""" def __init__(self, resource=None, client=None): - if resource is not None and not resource.startswith('/'): - resource = '/' + resource + if resource is not None and not resource.startswith("/"): + resource = "/" + resource self.__resource = resource """ @@ -25,8 +24,9 @@ def __init__(self, resource=None, client=None): if client: self.__client = client - elif not hasattr(self, '__client'): + elif not hasattr(self, "__client"): from exonetapi import Client + self.__client = Client() def filter(self, filter_name, filter_value): @@ -35,7 +35,7 @@ def filter(self, filter_name, filter_value): :param filter_value: The value of the applied filter. :return: self """ - self.__query_params['filter[' + filter_name + ']'] = filter_value + self.__query_params["filter[" + filter_name + "]"] = filter_value return self def page(self, page_number): @@ -43,7 +43,7 @@ def page(self, page_number): :param page_number: The page number. :return: self """ - self.__query_params['page[number]'] = page_number + self.__query_params["page[number]"] = page_number return self def size(self, page_size): @@ -51,20 +51,20 @@ def size(self, page_size): :param page_size: The maximum number of returned resources. :return: self """ - self.__query_params['page[size]'] = page_size + self.__query_params["page[size]"] = page_size return self - def sort(self, sort_field, sort_order='asc'): + def sort(self, sort_field, sort_order="asc"): """Prepare this RequestBuilder to sort by a field. :param sort_field: The field name to sort on. :param sort_order: The order for sorting (asc/desc), default: asc. :return: self """ - if sort_order not in ['asc', 'desc']: + if sort_order not in ["asc", "desc"]: raise ValueError('Sort order can only be "asc" or "desc".') - self.__query_params['sort'] = '{sort}{field}'.format( - sort='-' if sort_order == 'desc' else '', + self.__query_params["sort"] = "{sort}{field}".format( + sort="-" if sort_order == "desc" else "", field=sort_field, ) return self @@ -74,14 +74,14 @@ def sort_asc(self, sort_field): :param sort_field: The field name to sort on. :return: self """ - return self.sort(sort_field, 'asc') + return self.sort(sort_field, "asc") def sort_desc(self, sort_field): """Prepare this RequestBuilder to sort by a field in descending order. :param sort_field: The field name to sort on. :return: self """ - return self.sort(sort_field, 'desc') + return self.sort(sort_field, "desc") def get(self, identifier=None): """Make a call to the API using the previously set options. @@ -90,9 +90,9 @@ def get(self, identifier=None): """ response = self.__make_call( - 'GET', + "GET", self.__build_url(identifier), - params=self.__query_params if not identifier else None + params=self.__query_params if not identifier else None, ) return Parser(response.content).parse() @@ -111,18 +111,23 @@ def post(self, resource): # If there are changed attributes, assume it s a new resource. if len(changed_attributes) > 0: - response = self.__make_call('POST', self.__build_url(), {'data': resource.to_json()}) + response = self.__make_call( + "POST", self.__build_url(), {"data": resource.to_json()} + ) return Parser(response.content).parse() - # If there are changed relations and no changed attributes, assume a POST to the relation. + # If there are changed relations and no changed attributes, assume a POST to + # the relation. if len(changed_relations) > 0: responses = [] for relation_name in changed_relations: response = self.__make_call( - 'POST', - '{}/relationships/{}'.format(self.__build_url(resource.id()), relation_name), - changed_relations[relation_name] + "POST", + "{}/relationships/{}".format( + self.__build_url(resource.id()), relation_name + ), + changed_relations[relation_name], ) responses.append(Parser(response.content).parse()) @@ -135,18 +140,18 @@ def patch(self, resource): # Patch changed attributes. if len(changed_attributes) > 0: self.__make_call( - 'PATCH', - self.__build_url(resource.id()), - {'data': changed_attributes} + "PATCH", self.__build_url(resource.id()), {"data": changed_attributes} ) # Patch changed relations. if len(changed_relations) > 0: for relation_name in changed_relations: self.__make_call( - 'PATCH', - '{}/relationships/{}'.format(self.__build_url(resource.id()), relation_name), - changed_relations[relation_name] + "PATCH", + "{}/relationships/{}".format( + self.__build_url(resource.id()), relation_name + ), + changed_relations[relation_name], ) return True @@ -156,15 +161,17 @@ def delete(self, resource): # If no relations are changed, DELETE the whole resource. if len(changed_relations) == 0: - self.__make_call('DELETE', self.__build_url(resource.id())) + self.__make_call("DELETE", self.__build_url(resource.id())) return True for relation_name in changed_relations: self.__make_call( - 'DELETE', - '{}/relationships/{}'.format(self.__build_url(resource.id()), relation_name), - changed_relations[relation_name] + "DELETE", + "{}/relationships/{}".format( + self.__build_url(resource.id()), relation_name + ), + changed_relations[relation_name], ) return True @@ -180,7 +187,7 @@ def __build_url(self, identifier=None): url += self.__resource if identifier: - url += '/' + identifier + url += "/" + identifier return url @@ -190,18 +197,14 @@ def __get_headers(self): :return: A dict with all the headers. """ return { - 'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer %s' % (self.__client.authenticator.get_token()) + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer %s" % (self.__client.authenticator.get_token()), } def __make_call(self, method, url, json_data=None, params=None): response = requests.request( - method, - url, - headers=self.__get_headers(), - json=json_data, - params=params + method, url, headers=self.__get_headers(), json=json_data, params=params ) # Handle validation errors. @@ -215,17 +218,17 @@ def __make_call(self, method, url, json_data=None, params=None): def __get_recursive(self, data=None, url=None): """ - Get the URL and call this method recursivly as long as there is an URL in the 'next' field - of the 'links' data. + Get the URL and call this method recursivly as long as there is an URL in the + 'next' field of the 'links' data. :param data: The ApiResourceSet to append the resources to. :param url: The URL to call. :return: The ApiResourceSet containing all requested resources. """ response = self.__make_call( - 'GET', + "GET", url or self.__build_url(), - params=self.__query_params if not url else None + params=self.__query_params if not url else None, ) content = Parser(response.content).parse() @@ -236,7 +239,7 @@ def __get_recursive(self, data=None, url=None): data.add_resource(content.resources()) - next_link = content.links().get('next') + next_link = content.links().get("next") if next_link is not None: return self.__get_recursive(data, next_link) diff --git a/exonetapi/__init__.py b/exonetapi/__init__.py old mode 100755 new mode 100644 index 5a9a4d0..c87089b --- a/exonetapi/__init__.py +++ b/exonetapi/__init__.py @@ -2,3 +2,5 @@ from .RequestBuilder import RequestBuilder from .result import Parser from .create_resource import create_resource + +__all__ = ["Client", "RequestBuilder", "Parser", "create_resource"] diff --git a/exonetapi/auth/Authenticator.py b/exonetapi/auth/Authenticator.py old mode 100755 new mode 100644 index a8dd95a..42b02f5 --- a/exonetapi/auth/Authenticator.py +++ b/exonetapi/auth/Authenticator.py @@ -20,7 +20,7 @@ def get_token(self): :return: The token if available. """ if self.__auth_details: - return self.__auth_details['access_token'].strip() + return self.__auth_details["access_token"].strip() def password_auth(self, username, password, client_id, client_secret): """Authorize using the password grant. @@ -31,25 +31,26 @@ def password_auth(self, username, password, client_id, client_secret): :param client_secret: The OAuth client secret. :return: None """ - self.get_new_token({ - 'grant_type': 'password', - 'username': username, - 'password': password, - 'client_id': client_id, - 'client_secret': client_secret - }) + self.get_new_token( + { + "grant_type": "password", + "username": username, + "password": password, + "client_id": client_id, + "client_secret": client_secret, + } + ) def set_token(self, token): """Set a token to use when authorizing. - This bypasses any calls to the authorization endpoint, but instead uses the provided token. + This bypasses any calls to the authorization endpoint, but instead uses the + provided token. :param token: A previously obtained token. :return: None """ - self.__auth_details = { - 'access_token': token - } + self.__auth_details = {"access_token": token} def get_new_token(self, payload): """Get a new token via the authorization endpoint. @@ -57,12 +58,10 @@ def get_new_token(self, payload): :param payload: :return: A new access token. """ - headers = {'Accept': 'application/vnd.Exonet.v1+json'} + headers = {"Accept": "application/vnd.Exonet.v1+json"} response = requests.post( - self.__host + self.__authentication_endpoint, - headers=headers, - data=payload + self.__host + self.__authentication_endpoint, headers=headers, data=payload ) # Raise exception on failed request. @@ -71,4 +70,4 @@ def get_new_token(self, payload): # Set the auth details in the request for later use. self.__auth_details = response.json() - return response.json()['access_token'] + return response.json()["access_token"] diff --git a/exonetapi/auth/__init__.py b/exonetapi/auth/__init__.py old mode 100755 new mode 100644 index 3c13333..703ff4c --- a/exonetapi/auth/__init__.py +++ b/exonetapi/auth/__init__.py @@ -1 +1,3 @@ from .Authenticator import Authenticator + +__all__ = ["Authenticator"] diff --git a/exonetapi/create_resource.py b/exonetapi/create_resource.py old mode 100755 new mode 100644 index dd072d4..4ce0079 --- a/exonetapi/create_resource.py +++ b/exonetapi/create_resource.py @@ -4,10 +4,9 @@ def create_resource(resource): """Create a dynamic Resource based on the type that is provided in the data. - :param resource: The resource. :return: A Resource instance. """ - resource_type = resource['type'] + resource_type = resource["type"] return type(camelize(resource_type), (ApiResource,), {})(resource) diff --git a/exonetapi/exceptions/ValidationException.py b/exonetapi/exceptions/ValidationException.py old mode 100755 new mode 100644 index 6e86d0e..d10df80 --- a/exonetapi/exceptions/ValidationException.py +++ b/exonetapi/exceptions/ValidationException.py @@ -11,25 +11,28 @@ def __init__(self, response): self.validation_errors = {} # Loop errors. - for error in response.json()['errors']: + for error in response.json()["errors"]: # Handle only validation errors. - if error['status'] == 422: - field = 'generic' - if 'field' in error['variables']: - field = error['variables']['field'] or error['detail'] + if error["status"] == 422: + field = "generic" + if "field" in error["variables"]: + field = error["variables"]["field"] or error["detail"] if field not in self.validation_errors: self.validation_errors[field] = [] - self.validation_errors[field].append(error['detail']) + self.validation_errors[field].append(error["detail"]) if self.validation_errors.__len__() == 1: - validation_error = 'There is {} validation error.' + validation_error = "There is {} validation error." else: - validation_error = 'There are {} validation errors.' + validation_error = "There are {} validation errors." - HTTPError.__init__(self, validation_error.format(self.validation_errors.__len__()), - response=response) + HTTPError.__init__( + self, + validation_error.format(self.validation_errors.__len__()), + response=response, + ) def get_failed_validations(self): return self.validation_errors diff --git a/exonetapi/exceptions/__init__.py b/exonetapi/exceptions/__init__.py old mode 100755 new mode 100644 index 83548a5..6f027a0 --- a/exonetapi/exceptions/__init__.py +++ b/exonetapi/exceptions/__init__.py @@ -1 +1,3 @@ from .ValidationException import ValidationException + +__all__ = ["ValidationException"] diff --git a/exonetapi/result/Parser.py b/exonetapi/result/Parser.py old mode 100755 new mode 100644 index ab0ba70..e0d6b77 --- a/exonetapi/result/Parser.py +++ b/exonetapi/result/Parser.py @@ -15,7 +15,7 @@ class Parser: def __init__(self, data): self.__data = data self.__json = json.loads(self.__data.decode()) - self.__json_data = self.__json.get('data') + self.__json_data = self.__json.get("data") def parse(self): """Parse JSON string into a ApiResource or a list of Resources. @@ -24,9 +24,9 @@ def parse(self): """ if type(self.__json_data) is list: resources = ApiResourceSet() - resources \ - .set_meta(self.__json.get('meta')) \ - .set_links(self.__json.get('links')) + resources.set_meta(self.__json.get("meta")).set_links( + self.__json.get("links") + ) for resource_data in self.__json_data: resource = self.make_resource(resource_data) @@ -37,24 +37,21 @@ def parse(self): return self.make_resource(self.__json_data) def make_resource(self, resource_data): - resource = create_resource({ - 'type': resource_data['type'], - 'id': resource_data['id'] - }) + resource = create_resource( + {"type": resource_data["type"], "id": resource_data["id"]} + ) # Set attributes. - if 'attributes' in resource_data.keys(): - for attribute_name, attribute_value in resource_data['attributes'].items(): + if "attributes" in resource_data.keys(): + for attribute_name, attribute_value in resource_data["attributes"].items(): resource.attribute(attribute_name, attribute_value) resource.reset_changed_attributes() # Extract and parse all included relations. - if 'relationships' in resource_data.keys(): + if "relationships" in resource_data.keys(): parsed_relations = self.parse_relations( - resource_data['relationships'], - resource.type(), - resource.id() + resource_data["relationships"], resource.type(), resource.id() ) for k, r in parsed_relations.items(): @@ -70,22 +67,25 @@ def parse_relations(self, relationships, origin_type, origin_id): if relationships: for relationName, relation in relationships.items(): # Set a relation - if ('data' in relation.keys()) and relation['data']: + if ("data" in relation.keys()) and relation["data"]: relationship = Relationship(relationName, origin_type, origin_id) # Set a single relationship. - if 'type' in relation['data']: + if "type" in relation["data"]: relationship.set_resource_identifiers( - ApiResourceIdentifier(relation['data']['type'], relation['data']['id']) + ApiResourceIdentifier( + relation["data"]["type"], relation["data"]["id"] + ) ) # Set a multi relationship. - elif isinstance(relation['data'], list): + elif isinstance(relation["data"], list): relationships = [] - for relationItem in relation['data']: - relationships.append(ApiResourceIdentifier( - relationItem['type'], - relationItem['id']) + for relationItem in relation["data"]: + relationships.append( + ApiResourceIdentifier( + relationItem["type"], relationItem["id"] + ) ) relationship.set_resource_identifiers(relationships) diff --git a/exonetapi/result/__init__.py b/exonetapi/result/__init__.py old mode 100755 new mode 100644 index a9bbb26..7e1aab5 --- a/exonetapi/result/__init__.py +++ b/exonetapi/result/__init__.py @@ -1 +1,3 @@ from .Parser import Parser + +__all__ = ["Parser"] diff --git a/exonetapi/structures/ApiResource.py b/exonetapi/structures/ApiResource.py old mode 100755 new mode 100644 index 7153f69..736caf9 --- a/exonetapi/structures/ApiResource.py +++ b/exonetapi/structures/ApiResource.py @@ -6,22 +6,22 @@ class ApiResource(ApiResourceIdentifier): - """Basic Resource with attributes. - """ + """Basic Resource with attributes.""" + def __init__(self, data_or_type, resource_id=None): data = dict() if type(data_or_type) is str: - data['type'] = data_or_type - data['id'] = resource_id + data["type"] = data_or_type + data["id"] = resource_id elif type(data_or_type) is dict: - data['type'] = data_or_type['type'] - data['id'] = data_or_type['id'] if 'id' in data_or_type else None + data["type"] = data_or_type["type"] + data["id"] = data_or_type["id"] if "id" in data_or_type else None else: raise ValueError("First argument must be a string or dict.") # Call parent init method. - super().__init__(data['type'], data['id']) + super().__init__(data["type"], data["id"]) self.__changed_attributes = [] self.__attributes = {} @@ -53,17 +53,16 @@ def to_json(self): :return: The dict with attributes according to JSON-API spec. """ json = { - 'type': self.type(), - 'attributes': self.attributes(), - + "type": self.type(), + "attributes": self.attributes(), } if self.id(): - json['id'] = self.id() + json["id"] = self.id() relationships = self.get_json_relationships() if relationships: - json['relationships'] = relationships + json["relationships"] = relationships return json @@ -82,12 +81,12 @@ def to_json_changed_attributes(self): attributes[changed_attribute] = self.attributes().get(changed_attribute) json = { - 'type': self.type(), - 'attributes': attributes, + "type": self.type(), + "attributes": attributes, } if self.id(): - json['id'] = self.id() + json["id"] = self.id() return json diff --git a/exonetapi/structures/ApiResourceIdentifier.py b/exonetapi/structures/ApiResourceIdentifier.py old mode 100755 new mode 100644 index d014ce2..35af804 --- a/exonetapi/structures/ApiResourceIdentifier.py +++ b/exonetapi/structures/ApiResourceIdentifier.py @@ -7,8 +7,7 @@ class ApiResourceIdentifier(object): - """Basic ApiResource identifier. - """ + """Basic ApiResource identifier.""" def __init__(self, type, id=None): """Initialize the resource. @@ -54,14 +53,16 @@ def related(self, name): def relationship(self, name, *data): """Define a new relationship for this resource, replace an existing one or get an - existing one. When data is provided the relationship is set, without data the relationship - is returned. + existing one. When data is provided the relationship is set, without data the + relationship is returned. :param name: The name of the relation to set. - :param data: The value of the relation, can be a ApiResource or a dict of Resources. - :return self: when setting a relationship, or the actual relationship when getting it + :param data: The value of the relation, can be a ApiResource or a dict + of Resources. + :return self: when setting a relationship, or the actual relationship when + getting it """ - if len(data) is 1: + if len(data) == 1: return self.set_relationship(name, data[0]) return self.get_relationship(name) @@ -82,7 +83,8 @@ def set_relationship(self, name, data): Can be a relation to a single ApiResource or a dict of Resources. :param name: The name of the relation to set. - :param data: The value of the relation, can be a ApiResource or a dict of Resources. + :param data: The value of the relation, can be a ApiResource or a dict of + Resources. :return: self """ @@ -97,8 +99,8 @@ def to_json(self): :return: A dict with the resource type and ID. """ return { - 'type': self.type(), - 'id': self.id(), + "type": self.type(), + "id": self.id(), } def get_json_relationships(self, only_changed_relations=False): @@ -109,7 +111,10 @@ def get_json_relationships(self, only_changed_relations=False): relationships = {} for relation_name, relation in self.__relationships.items(): - if only_changed_relations is True and relation_name not in self.__changed_relations: + if ( + only_changed_relations is True + and relation_name not in self.__changed_relations + ): continue relationships[relation_name] = {} @@ -121,14 +126,16 @@ def get_json_relationships(self, only_changed_relations=False): except AttributeError: identifier = relation_resource.to_json() relation_list.append(identifier) - relationships[relation_name]['data'] = relation_list + relationships[relation_name]["data"] = relation_list elif type(relation) is dict: - relationships[relation_name]['data'] = relation['data'] + relationships[relation_name]["data"] = relation["data"] else: try: - relationships[relation_name]['data'] = relation.to_json_resource_identifier() + relationships[relation_name][ + "data" + ] = relation.to_json_resource_identifier() except AttributeError: - relationships[relation_name]['data'] = relation.to_json() + relationships[relation_name]["data"] = relation.to_json() return relationships diff --git a/exonetapi/structures/ApiResourceSet.py b/exonetapi/structures/ApiResourceSet.py old mode 100755 new mode 100644 index 43a11f1..90a212c --- a/exonetapi/structures/ApiResourceSet.py +++ b/exonetapi/structures/ApiResourceSet.py @@ -9,7 +9,7 @@ def __init__(self): self.__iter_current = 0 def total(self): - return self.__meta.get('resources', {"total": None}).get('total') + return self.__meta.get("resources", {"total": None}).get("total") def links(self): return self.__links @@ -18,16 +18,16 @@ def meta(self): return self.__meta def next_page(self): - return self.__get_link('next') + return self.__get_link("next") def previous_page(self): - return self.__get_link('prev') + return self.__get_link("prev") def first_page(self): - return self.__get_link('first') + return self.__get_link("first") def last_page(self): - return self.__get_link('last') + return self.__get_link("last") def add_resource(self, resource): if isinstance(resource, list): @@ -54,7 +54,7 @@ def __get_link(self, link_name): link_value = self.__links.get(link_name) if link_value is not None: - return request.get(link_value.replace('{}/'.format(host), '')) + return request.get(link_value.replace("{}/".format(host), "")) else: return None diff --git a/exonetapi/structures/Relation.py b/exonetapi/structures/Relation.py index f423ed8..5dfe37a 100644 --- a/exonetapi/structures/Relation.py +++ b/exonetapi/structures/Relation.py @@ -1,6 +1,6 @@ class Relation(object): # string Pattern to create the relation url. - __urlPattern = '/%s/%s/%s' + __urlPattern = "/%s/%s/%s" def __init__(self, relation_name, origin_type, origin_id): """Relation constructor. @@ -12,10 +12,12 @@ def __init__(self, relation_name, origin_type, origin_id): self.__name = relation_name self.__url = self.__urlPattern % (origin_type, origin_id, relation_name) - # ApiResourceSet|ApiResourceIdentifier The related resource identifier or a ApiResourceSet. + # ApiResourceSet|ApiResourceIdentifier The related resource identifier or a + # ApiResourceSet. self.__resourceIdentifiers = None from exonetapi import RequestBuilder + self.__request = RequestBuilder(self.__url) def __len__(self): @@ -34,7 +36,8 @@ def get_resource_identifiers(self): """ Get the resource identifiers for this relation. - :return ApiResourceSet|ApiResourceIdentifier: The resource identifier or a resource set. + :return ApiResourceSet|ApiResourceIdentifier: The resource identifier or a + resource set. """ return self.__resourceIdentifiers @@ -42,7 +45,8 @@ def set_resource_identifiers(self, new_relationship): """ Replace the related resource identifiers with new data. - :param ApiResourceSet|ApiResourceIdentifier new_relationship: A new resource identifier or a + :param ApiResourceSet|ApiResourceIdentifier new_relationship: A new resource + identifier or a new resource set. :return self: """ diff --git a/exonetapi/structures/Relationship.py b/exonetapi/structures/Relationship.py index 857098a..a28a81b 100644 --- a/exonetapi/structures/Relationship.py +++ b/exonetapi/structures/Relationship.py @@ -4,4 +4,4 @@ class Relationship(Relation): # string Pattern to create the relationship url. - __urlPattern = '/%s/%s/relationships/%s' + __urlPattern = "/%s/%s/relationships/%s" diff --git a/exonetapi/structures/__init__.py b/exonetapi/structures/__init__.py old mode 100755 new mode 100644 index 3ce42b8..298c3f1 --- a/exonetapi/structures/__init__.py +++ b/exonetapi/structures/__init__.py @@ -1,3 +1,13 @@ from .ApiResource import ApiResource from .ApiResourceIdentifier import ApiResourceIdentifier from .ApiResourceSet import ApiResourceSet +from .Relation import Relation +from .Relationship import Relationship + +__all__ = [ + "ApiResource", + "ApiResourceIdentifier", + "ApiResourceSet", + "Relation", + "Relationship", +] diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..61db7a9 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,702 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "bandit" +version = "1.7.4" +description = "Security oriented static analyser for python code." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +GitPython = ">=1.0.1" +PyYAML = ">=5.3.1" +stevedore = ">=1.20.0" + +[package.extras] +test = ["coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "toml", "beautifulsoup4 (>=4.8.0)", "pylint (==1.9.4)"] +toml = ["toml"] +yaml = ["pyyaml"] + +[[package]] +name = "black" +version = "22.1.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = ">=1.1.0" +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.4" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.3.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "gitdb" +version = "4.0.9" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.27" +description = "GitPython is a python library used to interact with Git repositories" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "4.2.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "inflection" +version = "0.5.1" +description = "A port of Ruby on Rails inflector to Python" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "pbr" +version = "5.8.1" +description = "Python Build Reasonableness" +category = "dev" +optional = false +python-versions = ">=2.6" + +[[package]] +name = "platformdirs" +version = "2.5.1" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "3.0.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "7.0.1" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "stevedore" +version = "3.5.0" +description = "Manage dynamic plugins for Python applications" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typed-ast" +version = "1.5.2" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.1.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "urllib3" +version = "1.26.8" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "zipp" +version = "3.7.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "6ec55cd0090f6aa24f1e55e5ec7b682ab1f12fb4559e9317c0ca5a4a5b398d2a" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +bandit = [ + {file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"}, + {file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"}, +] +black = [ + {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, + {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, + {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, + {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, + {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, + {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, + {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, + {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, + {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, + {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, + {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, + {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, + {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, + {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, + {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, + {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, + {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, + {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, + {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, + {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, + {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, + {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, + {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +click = [ + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, + {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, + {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, + {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, + {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, + {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, + {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, + {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, + {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, + {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, + {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, + {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, +] +flake8 = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] +gitdb = [ + {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, + {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, +] +gitpython = [ + {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"}, + {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, +] +inflection = [ + {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, + {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +pbr = [ + {file = "pbr-5.8.1-py2.py3-none-any.whl", hash = "sha256:27108648368782d07bbf1cb468ad2e2eeef29086affd14087a6d04b7de8af4ec"}, + {file = "pbr-5.8.1.tar.gz", hash = "sha256:66bc5a34912f408bb3925bf21231cb6f59206267b7f63f3503ef865c1a292e25"}, +] +platformdirs = [ + {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, + {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] +pyparsing = [ + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, +] +pytest = [ + {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, + {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +smmap = [ + {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, + {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, +] +stevedore = [ + {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"}, + {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typed-ast = [ + {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"}, + {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"}, + {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"}, + {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"}, + {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"}, + {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"}, + {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"}, + {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"}, + {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"}, + {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"}, + {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"}, + {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"}, + {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"}, + {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"}, + {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"}, + {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"}, + {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"}, + {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"}, + {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"}, + {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"}, + {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"}, + {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"}, + {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"}, + {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"}, +] +typing-extensions = [ + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, +] +urllib3 = [ + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, +] +zipp = [ + {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, + {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8434fce --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[tool.poetry] +authors = ["Exonet "] +classifiers = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', +] +description = "Library to interact with the Exonet API." +license = "MIT" +name = "exonetapi" +packages = [ + {include = "exonetapi"}, + {include = "exonetapi/**/*.py"}, +] +version = "4.0.0" + +[tool.poetry.dependencies] +inflection = "^0.5.1" +python = "^3.7" +requests = "^2.27.1" + +[tool.poetry.dev-dependencies] +black = {version = "^22.1.0", allow-prereleases = true} +flake8 = "^4.0.1" +pytest = "^7.0.1" +pytest-cov = "^3.0.0" +bandit = "^1.7.4" + +[build-system] +build-backend = "poetry.core.masonry.api" +requires = ["poetry-core>=1.0.0"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100755 index 6e31b18..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -certifi==2020.6.20 -chardet==3.0.4 -coverage==5.3 -idna==2.10 -inflection==0.5.1 -requests==2.26.0 -urllib3==1.26.6 -pycodestyle==2.6.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 93f68ae..0000000 --- a/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[pycodestyle] -max-line-length = 100 -statistics = True -exclude=./dev/* diff --git a/setup.py b/setup.py deleted file mode 100755 index 3b2c9ce..0000000 --- a/setup.py +++ /dev/null @@ -1,53 +0,0 @@ -"""A setuptools based setup module. - -See: -https://packaging.python.org/en/latest/distributing.html -""" - -# Prefer setuptools over distutils. -from setuptools import setup, find_packages -# To use a consistent encoding. -from codecs import open -from os import path - - -here = path.abspath(path.dirname(__file__)) -# Get the long description from the README file -with open(path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() - -setup( - name='exonetapi', - version='3.0.3', - - description='Library to interact with the Exonet API.', - long_description=long_description, - keywords='Exonet API', - - url='https://github.com/exonet/exonet-api-python', - - author='Exonet B.V.', - author_email='development@exonet.nl', - - license='MIT', - - python_requires='~=3.4', - - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - ], - - packages=find_packages(exclude=['contrib', 'docs', 'tests']), - package_dir={'exonetapi': 'exonetapi'}, - install_requires=['requests', 'inflection'], -) diff --git a/tests/__init__.py b/tests/__init__.py old mode 100755 new mode 100644 diff --git a/tests/auth/__init__.py b/tests/auth/__init__.py old mode 100755 new mode 100644 diff --git a/tests/auth/testAuthenticator.py b/tests/auth/testAuthenticator.py deleted file mode 100755 index fec2c07..0000000 --- a/tests/auth/testAuthenticator.py +++ /dev/null @@ -1,70 +0,0 @@ -import unittest -from unittest import mock -from unittest.mock import MagicMock - -from tests.testCase import testCase -from exonetapi.auth import Authenticator - - -class testAuthenticator(testCase): - class MockResponse: - def __init__(self, content, status_code=200): - self.content = content - self.status_code = status_code - - def raise_for_status(self): - return None - - @mock.patch('requests.post') - def test_get_new_token(self, mock_requests_post): - mock_requests_post.return_value = self.MockResponse('new token') - mock_requests_post.return_value.json = MagicMock(return_value={ - 'access_token': 'new token' - }) - - a = Authenticator('https://test.url/', 'auth') - token = a.get_new_token('payload') - - mock_requests_post.assert_called_with( - 'https://test.url/auth', - headers={ - 'Accept': 'application/vnd.Exonet.v1+json', - }, - data='payload' - ) - - self.assertEqual(token, 'new token') - - def test_set_token(self): - a = Authenticator('https://test.url/', 'auth') - a.set_token('a-new-token') - - self.assertEqual(a.get_token(), 'a-new-token') - - @mock.patch('requests.post') - def test_password_auth(self, mock_requests_post): - a = Authenticator('https://test.url/', 'auth') - a.password_auth( - 'user', - 'pwd', - 'client id', - 'client secret' - ) - - mock_requests_post.assert_called_with( - 'https://test.url/auth', - headers={ - 'Accept': 'application/vnd.Exonet.v1+json', - }, - data={ - 'grant_type': 'password', - 'username': 'user', - 'password': 'pwd', - 'client_id': 'client id', - 'client_secret': 'client secret' - } - ) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/auth/test_authenticator.py b/tests/auth/test_authenticator.py new file mode 100644 index 0000000..5eb54cf --- /dev/null +++ b/tests/auth/test_authenticator.py @@ -0,0 +1,65 @@ +import unittest +from unittest import mock +from unittest.mock import MagicMock + +from tests.testCase import testCase +from exonetapi.auth import Authenticator + + +class testAuthenticator(testCase): + class MockResponse: + def __init__(self, content, status_code=200): + self.content = content + self.status_code = status_code + + def raise_for_status(self): + return None + + @mock.patch("requests.post") + def test_get_new_token(self, mock_requests_post): + mock_requests_post.return_value = self.MockResponse("new token") + mock_requests_post.return_value.json = MagicMock( + return_value={"access_token": "new token"} + ) + + a = Authenticator("https://test.url/", "auth") + token = a.get_new_token("payload") + + mock_requests_post.assert_called_with( + "https://test.url/auth", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + }, + data="payload", + ) + + self.assertEqual(token, "new token") + + def test_set_token(self): + a = Authenticator("https://test.url/", "auth") + a.set_token("a-new-token") + + self.assertEqual(a.get_token(), "a-new-token") + + @mock.patch("requests.post") + def test_password_auth(self, mock_requests_post): + a = Authenticator("https://test.url/", "auth") + a.password_auth("user", "pwd", "client id", "client secret") + + mock_requests_post.assert_called_with( + "https://test.url/auth", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + }, + data={ + "grant_type": "password", + "username": "user", + "password": "pwd", + "client_id": "client id", + "client_secret": "client secret", + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/exceptions/__init__.py b/tests/exceptions/__init__.py old mode 100755 new mode 100644 diff --git a/tests/exceptions/testValidationException.py b/tests/exceptions/test_validation_exception.py old mode 100755 new mode 100644 similarity index 68% rename from tests/exceptions/testValidationException.py rename to tests/exceptions/test_validation_exception.py index 6113225..12100f1 --- a/tests/exceptions/testValidationException.py +++ b/tests/exceptions/test_validation_exception.py @@ -12,16 +12,12 @@ def test_no_errors(self): # Construct the request response. response = create_autospec(Response, spec_set=True) - response.json = Mock( - return_value={ - 'errors': [] - } - ) + response.json = Mock(return_value={"errors": []}) v = ValidationException(response) response.json.assert_called_once() - self.assertEqual(v.args[0], 'There are 0 validation errors.') + self.assertEqual(v.args[0], "There are 0 validation errors.") def test_one_error(self): # Construct the request response. @@ -29,15 +25,15 @@ def test_one_error(self): response.json = Mock( return_value={ - 'errors': [ + "errors": [ { - 'status': 422, - 'detail': 'Detailed error message', - 'variables': { - 'field': 'start_date', - 'rule': 'iso8601-date', - 'rule_requirement': 'Date must be in iso8601 format' - } + "status": 422, + "detail": "Detailed error message", + "variables": { + "field": "start_date", + "rule": "iso8601-date", + "rule_requirement": "Date must be in iso8601 format", + }, } ] } @@ -48,8 +44,10 @@ def test_one_error(self): response.json.assert_called_once() # Make sure the right message is set. - self.assertEqual(v.args[0], 'There is 1 validation error.') - self.assertEqual(v.get_failed_validations()['start_date'][0], 'Detailed error message') + self.assertEqual(v.args[0], "There is 1 validation error.") + self.assertEqual( + v.get_failed_validations()["start_date"][0], "Detailed error message" + ) def test_twoErrors(self): # Construct the request response. @@ -67,16 +65,16 @@ def test_twoErrors(self): "field": "data.end_date", "value": None, "rule": "Required", - "rule_requirement": "" - } + "rule_requirement": "", + }, }, { "status": 422, "code": "102.10001", "title": "validation.generic", "detail": "The provided data is invalid.", - "variables": [] - } + "variables": [], + }, ] } ) @@ -87,9 +85,11 @@ def test_twoErrors(self): response.json.assert_called_once() failed = v.get_failed_validations() # Make sure the right message is set. - self.assertEqual(v.args[0], 'There are 2 validation errors.') - self.assertEqual(failed['data.end_date'][0], 'The data.end_date field is required.') - self.assertEqual(failed['generic'][0], 'The provided data is invalid.') + self.assertEqual(v.args[0], "There are 2 validation errors.") + self.assertEqual( + failed["data.end_date"][0], "The data.end_date field is required." + ) + self.assertEqual(failed["generic"][0], "The provided data is invalid.") def test_otherErrors(self): # Construct the request response. @@ -110,8 +110,8 @@ def test_otherErrors(self): response.json.assert_called_once() # Make sure there is no validation exception message. - self.assertEqual(v.args[0], 'There are 0 validation errors.') + self.assertEqual(v.args[0], "There are 0 validation errors.") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/result/__init__.py b/tests/result/__init__.py old mode 100755 new mode 100644 diff --git a/tests/result/testParser.py b/tests/result/test_parser.py old mode 100755 new mode 100644 similarity index 84% rename from tests/result/testParser.py rename to tests/result/test_parser.py index d622170..ca831f1 --- a/tests/result/testParser.py +++ b/tests/result/test_parser.py @@ -49,15 +49,15 @@ def test_parse_list(self): } ] } - """ + """ # noqa: E501 result = Parser(str.encode(json_data_list)).parse().resources() - self.assertEqual(result[0].id(), 'DV6axK4GwNEb') - self.assertEqual(result[0].type(), 'comments') + self.assertEqual(result[0].id(), "DV6axK4GwNEb") + self.assertEqual(result[0].type(), "comments") - self.assertEqual(result[1].id(), 'zWX9r7exA28G') - self.assertEqual(result[1].type(), 'comments') + self.assertEqual(result[1].id(), "zWX9r7exA28G") + self.assertEqual(result[1].type(), "comments") def test_parse_single(self): json_data_list = """ @@ -83,12 +83,12 @@ def test_parse_single(self): } } } - """ + """ # noqa: E501 result = Parser(str.encode(json_data_list)).parse() - self.assertEqual(result.id(), 'DV6axK4GwNEb') - self.assertEqual(result.type(), 'comments') + self.assertEqual(result.id(), "DV6axK4GwNEb") + self.assertEqual(result.type(), "comments") def test_parse_single_with_multi_relation(self): json_data_list = """ @@ -119,9 +119,14 @@ def test_parse_single_with_multi_relation(self): } } } - """ + """ # noqa: E501 - result = Parser(str.encode(json_data_list)).parse().relationship('tags').get_resource_identifiers() + result = ( + Parser(str.encode(json_data_list)) + .parse() + .relationship("tags") + .get_resource_identifiers() + ) self.assertEqual(len(result), 2) @@ -156,12 +161,17 @@ def test_parse_single_with_multi_relation(self): } } } - """ + """ # noqa: E501 - result = Parser(json_data_list).parse().relationship('tags').get_resource_identifiers() + result = ( + Parser(json_data_list) + .parse() + .relationship("tags") + .get_resource_identifiers() + ) self.assertEqual(len(result), 2) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/structures/__init__.py b/tests/structures/__init__.py old mode 100755 new mode 100644 diff --git a/tests/structures/testApiResource.py b/tests/structures/testApiResource.py deleted file mode 100755 index 0f0f247..0000000 --- a/tests/structures/testApiResource.py +++ /dev/null @@ -1,170 +0,0 @@ -import unittest -from unittest.mock import MagicMock -from unittest import mock - -from tests.testCase import testCase - -from exonetapi.structures.ApiResource import ApiResource -from exonetapi import create_resource - -import json - - -class testApiResource(testCase): - - def test_init(self): - resource = create_resource({ - 'type': 'fake', - }) - resource.attribute('first_name', 'John') - resource.attribute('last_name', 'Doe') - - self.assertEqual(resource.attribute('first_name'), 'John') - self.assertEqual(resource.type(), 'fake') - self.assertIsNone(resource.id()) - self.assertEqual(resource.attributes(), {'first_name': 'John', 'last_name': 'Doe', }) - - def test_init_string(self): - resource = ApiResource('fake', 'abc') - resource.attribute('first_name', 'John') - resource.attribute('last_name', 'Doe') - - self.assertEqual(resource.attribute('first_name'), 'John') - self.assertEqual(resource.type(), 'fake') - self.assertEqual(resource.id(), 'abc') - self.assertEqual(resource.attributes(), {'first_name': 'John', 'last_name': 'Doe', }) - - def test_init_invalid(self): - with self.assertRaises(ValueError): - ApiResource(1234) - - def test_init_relationship(self): - resource = create_resource({ - 'type': 'fake', - }) - - resource.set_relationship( - 'account', - create_resource({ - 'type': 'account', - 'id': 'someAccountID', - }) - ) - - self.assertEqual(resource.get_json_relationships(), { - 'account': {'data': {'type': 'account', 'id': 'someAccountID'}} - }) - - def test_set_relationship(self): - resource = create_resource({ - 'type': 'fake' - }) - - resource.relationship('messages', [ - create_resource({ - 'type': 'message', - 'id': 'messageOne' - }), - create_resource({ - 'type': 'message', - 'id': 'messageTwo' - }) - ]) - - self.assertEqual(resource.get_json_relationships(), { - 'messages': {'data': [ - {'id': 'messageOne', 'type': 'message'}, - {'id': 'messageTwo', 'type': 'message'} - ]} - }) - - def test_to_json(self): - resource = ApiResource({ - 'type': 'fake', - 'id': 'FakeID', - }) - resource.set_relationship( - 'thing', - create_resource({ - 'type': 'things', - 'id': 'thingID', - }) - ) - - self.assertEqual( - json.dumps(resource.to_json()), - json.dumps({ - 'type': 'fake', - 'attributes': {}, - 'id': 'FakeID', - 'relationships': { - 'thing': { - 'data': { - 'type': 'things', - 'id': 'thingID' - } - } - } - }) - ) - - def test_to_json_changed_attributes(self): - resource = ApiResource({ - 'type': 'fake', - 'id': 'FakeID', - }) - self.assertEqual({}, resource.to_json_changed_attributes()) - - resource.attribute('test', 'Hello World') - resource.set_relationship( - 'thing', - create_resource({ - 'type': 'things', - 'id': 'thingID', - }) - ) - - self.assertEqual( - json.dumps({"type": "fake", "attributes": {"test": "Hello World"}, "id": "FakeID"}), - json.dumps(resource.to_json_changed_attributes()) - ) - - @mock.patch('exonetapi.RequestBuilder.__init__', return_value=None) - @mock.patch('exonetapi.RequestBuilder.patch') - def test_patch(self, mock_requestbuilder_patch, mock_requestbuilder_init): - mock_requestbuilder_patch.patch = MagicMock(return_value=None) - ApiResource({'type': 'fake', 'id': 'FakeID'}).patch() - mock_requestbuilder_init.assert_called_with('fake') - - @mock.patch('exonetapi.RequestBuilder.__init__', return_value=None) - @mock.patch('exonetapi.RequestBuilder.delete') - def test_delete(self, mock_requestbuilder_delete, mock_requestbuilder_init): - mock_requestbuilder_delete.delete = MagicMock(return_value=None) - ApiResource({'type': 'fake', 'id': 'FakeID'}).delete() - mock_requestbuilder_init.assert_called_with('fake') - - @mock.patch('exonetapi.RequestBuilder.__init__', return_value=None) - @mock.patch('exonetapi.RequestBuilder.post') - def test_post(self, mock_requestbuilder_post, mock_requestbuilder_init): - mock_requestbuilder_post.post = MagicMock(return_value=None) - ApiResource({'type': 'fake', 'id': 'FakeID'}).post() - mock_requestbuilder_init.assert_called_with('fake') - - def test_reset_changed_attributes(self): - resource = ApiResource({ - 'type': 'fake', - 'id': 'FakeID', - }) - resource.attribute('test', 'Hello World') - - self.assertEqual( - {'attributes': {'test': 'Hello World'}, 'id': 'FakeID', 'type': 'fake'}, - resource.to_json_changed_attributes() - ) - - resource.reset_changed_attributes() - self.assertEqual({}, resource.to_json_changed_attributes()) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/structures/testApiResourceIdentifier.py b/tests/structures/testApiResourceIdentifier.py deleted file mode 100755 index 1934948..0000000 --- a/tests/structures/testApiResourceIdentifier.py +++ /dev/null @@ -1,160 +0,0 @@ -import unittest -from unittest import mock -from unittest.mock import MagicMock - -from tests.testCase import testCase - -from exonetapi.structures import ApiResourceIdentifier -from exonetapi.structures.ApiResource import ApiResource -from exonetapi.structures.Relationship import Relationship -from exonetapi.structures.Relation import Relation -from exonetapi import create_resource - -import json - - -class testApiResourceIdentifier(testCase): - - def test_init(self): - resource = create_resource({ - 'type': 'fake', - }) - resource.attribute('first_name', 'John') - resource.attribute('last_name', 'Doe') - - self.assertEqual(resource.attribute('first_name'), 'John') - self.assertEqual(resource.type(), 'fake') - self.assertIsNone(resource.id()) - self.assertEqual(resource.attributes(), {'first_name': 'John', 'last_name': 'Doe', }) - - def test_get_relationship_create(self): - resource = create_resource({ - 'type': 'fake', - }) - - relationship = resource.get_relationship('something') - self.assertIsInstance(relationship, Relationship) - - def test_set_relationship(self): - resource = create_resource({ - 'type': 'fake' - }) - - resource.relationship('messages', [ - create_resource({ - 'type': 'message', - 'id': 'messageOne' - }), - create_resource({ - 'type': 'message', - 'id': 'messageTwo' - }) - ]) - - self.assertEqual(resource.get_json_relationships(), { - 'messages': {'data': [ - {'id': 'messageOne', 'type': 'message'}, - {'id': 'messageTwo', 'type': 'message'} - ]} - }) - - def test_get_json_relationships(self): - resource = create_resource({ - 'type': 'fake' - }) - resource.relationship('ignored', { - 'data': { - 'type': 'this', - 'id': 'that', - } - }) - resource.reset_changed_relations() - - resource.relationship('object', {'data': {'type': 'this', 'id': 'that'}}) - resource.relationship('resource', ApiResource('this', 'that')) - resource.relationship('resource_identifier', ApiResourceIdentifier('this', 'that')) - resource.relationship('list', [ApiResourceIdentifier('this', 'that')]) - - self.assertEqual( - resource.get_json_changed_relationships(), - { - 'object': {'data': {'id': 'that', 'type': 'this'}}, - 'resource': {'data': {'id': 'that', 'type': 'this'}}, - 'resource_identifier': {'data': {'id': 'that', 'type': 'this'}}, - 'list': {'data': [{'id': 'that', 'type': 'this'}]}, - } - ) - - def test_get_json_changed_relationships(self): - resource = create_resource({ - 'type': 'fake' - }) - - resource.relationship('messages', { - 'data': { - 'type': 'this', - 'id': 'that', - } - }) - - self.assertEqual( - resource.get_json_changed_relationships(), - { - 'messages': { - 'data': { - 'id': 'that', - 'type': 'this' - } - } - } - ) - - def test_to_json(self): - resource = ApiResource({ - 'type': 'fake', - 'id': 'FakeID', - }) - resource.set_relationship( - 'thing', - create_resource({ - 'type': 'things', - 'id': 'thingID', - }) - ) - - self.assertEqual( - json.dumps(resource.to_json()), - json.dumps({ - 'type': 'fake', - 'attributes': {}, - 'id': 'FakeID', - 'relationships': { - 'thing': { - 'data': { - 'type': 'things', - 'id': 'thingID' - } - } - } - }) - ) - - def test_related(self): - resource = create_resource({ - 'type': 'fake', - }) - - relation = resource.related('something') - self.assertIsInstance(relation, Relation) - - @mock.patch('exonetapi.RequestBuilder.__init__', return_value=None) - @mock.patch('exonetapi.RequestBuilder.get') - def test_post(self, mock_requestbuilder_get, mock_requestbuilder_init): - mock_requestbuilder_get.get = MagicMock(return_value=None) - ApiResource({'type': 'fake', 'id': 'FakeID'}).get() - mock_requestbuilder_get.assert_called_with('FakeID') - mock_requestbuilder_init.assert_called_with('fake') - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/structures/testApiResourceSet.py b/tests/structures/testApiResourceSet.py deleted file mode 100755 index 7a87acd..0000000 --- a/tests/structures/testApiResourceSet.py +++ /dev/null @@ -1,73 +0,0 @@ -import unittest -from unittest import mock - -from exonetapi.structures.ApiResourceSet import ApiResourceSet -from exonetapi.structures.ApiResource import ApiResource -from tests.testCase import testCase - - -class testApiResourceSet(testCase): - - def test_total(self): - api_resource_set = ApiResourceSet() - meta = {'resources': {'total': 1337}} - api_resource_set.set_meta(meta) - - self.assertEqual(1337, api_resource_set.total()) - self.assertEqual(meta, api_resource_set.meta()) - - def test_links(self): - links = { - 'next': 'next_link', - 'prev': 'prev_link' - } - api_resource_set = ApiResourceSet() - api_resource_set.set_links(links) - - self.assertEqual(links, api_resource_set.links()) - - @mock.patch('exonetapi.RequestBuilder.get', return_value='api_response') - def test_pagination(self, mock_request_builder): - links = { - 'next': 'https://api.exonet.nl/next_url?filter[unit]=test', - 'prev': 'https://api.exonet.nl/prev_url?filter[unit]=test', - 'first': 'https://api.exonet.nl/first/url?filter[unit]=test', - 'last': 'https://api.exonet.nl/last/url?filter[unit]=test&last=true', - } - api_resource_set = ApiResourceSet() - api_resource_set.set_links(links) - - self.assertEqual('api_response', api_resource_set.next_page()) - mock_request_builder.assert_called_with('next_url?filter[unit]=test') - - self.assertEqual('api_response', api_resource_set.previous_page()) - mock_request_builder.assert_called_with('prev_url?filter[unit]=test') - - self.assertEqual('api_response', api_resource_set.first_page()) - mock_request_builder.assert_called_with('first/url?filter[unit]=test') - - self.assertEqual('api_response', api_resource_set.last_page()) - mock_request_builder.assert_called_with('last/url?filter[unit]=test&last=true') - - api_resource_set.set_links({'next': None}) - self.assertEqual(None, api_resource_set.next_page()) - - def test_add_resource(self): - resource_one = ApiResource('fake', 'abc') - resource_two = ApiResource('fake', 'def') - - api_resource_set = ApiResourceSet() - api_resource_set.add_resource(resource_one) - api_resource_set.add_resource([resource_two]) - - self.assertEqual(2, len(api_resource_set)) - - for api_resource in api_resource_set: - self.assertTrue(api_resource.id() is 'abc' or api_resource.id() is 'def') - - for api_resource in api_resource_set.resources(): - self.assertTrue(api_resource.id() is 'abc' or api_resource.id() is 'def') - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/structures/test_api_resource.py b/tests/structures/test_api_resource.py new file mode 100644 index 0000000..2cbe0b7 --- /dev/null +++ b/tests/structures/test_api_resource.py @@ -0,0 +1,197 @@ +import unittest +from unittest.mock import MagicMock +from unittest import mock + +from tests.testCase import testCase + +from exonetapi.structures.ApiResource import ApiResource +from exonetapi import create_resource + +import json + + +class testApiResource(testCase): + def test_init(self): + resource = create_resource( + { + "type": "fake", + } + ) + resource.attribute("first_name", "John") + resource.attribute("last_name", "Doe") + + self.assertEqual(resource.attribute("first_name"), "John") + self.assertEqual(resource.type(), "fake") + self.assertIsNone(resource.id()) + self.assertEqual( + resource.attributes(), + { + "first_name": "John", + "last_name": "Doe", + }, + ) + + def test_init_string(self): + resource = ApiResource("fake", "abc") + resource.attribute("first_name", "John") + resource.attribute("last_name", "Doe") + + self.assertEqual(resource.attribute("first_name"), "John") + self.assertEqual(resource.type(), "fake") + self.assertEqual(resource.id(), "abc") + self.assertEqual( + resource.attributes(), + { + "first_name": "John", + "last_name": "Doe", + }, + ) + + def test_init_invalid(self): + with self.assertRaises(ValueError): + ApiResource(1234) + + def test_init_relationship(self): + resource = create_resource( + { + "type": "fake", + } + ) + + resource.set_relationship( + "account", + create_resource( + { + "type": "account", + "id": "someAccountID", + } + ), + ) + + self.assertEqual( + resource.get_json_relationships(), + {"account": {"data": {"type": "account", "id": "someAccountID"}}}, + ) + + def test_set_relationship(self): + resource = create_resource({"type": "fake"}) + + resource.relationship( + "messages", + [ + create_resource({"type": "message", "id": "messageOne"}), + create_resource({"type": "message", "id": "messageTwo"}), + ], + ) + + self.assertEqual( + resource.get_json_relationships(), + { + "messages": { + "data": [ + {"id": "messageOne", "type": "message"}, + {"id": "messageTwo", "type": "message"}, + ] + } + }, + ) + + def test_to_json(self): + resource = ApiResource( + { + "type": "fake", + "id": "FakeID", + } + ) + resource.set_relationship( + "thing", + create_resource( + { + "type": "things", + "id": "thingID", + } + ), + ) + + self.assertEqual( + json.dumps(resource.to_json()), + json.dumps( + { + "type": "fake", + "attributes": {}, + "id": "FakeID", + "relationships": { + "thing": {"data": {"type": "things", "id": "thingID"}} + }, + } + ), + ) + + def test_to_json_changed_attributes(self): + resource = ApiResource( + { + "type": "fake", + "id": "FakeID", + } + ) + self.assertEqual({}, resource.to_json_changed_attributes()) + + resource.attribute("test", "Hello World") + resource.set_relationship( + "thing", + create_resource( + { + "type": "things", + "id": "thingID", + } + ), + ) + + self.assertEqual( + json.dumps( + {"type": "fake", "attributes": {"test": "Hello World"}, "id": "FakeID"} + ), + json.dumps(resource.to_json_changed_attributes()), + ) + + @mock.patch("exonetapi.RequestBuilder.__init__", return_value=None) + @mock.patch("exonetapi.RequestBuilder.patch") + def test_patch(self, mock_requestbuilder_patch, mock_requestbuilder_init): + mock_requestbuilder_patch.patch = MagicMock(return_value=None) + ApiResource({"type": "fake", "id": "FakeID"}).patch() + mock_requestbuilder_init.assert_called_with("fake") + + @mock.patch("exonetapi.RequestBuilder.__init__", return_value=None) + @mock.patch("exonetapi.RequestBuilder.delete") + def test_delete(self, mock_requestbuilder_delete, mock_requestbuilder_init): + mock_requestbuilder_delete.delete = MagicMock(return_value=None) + ApiResource({"type": "fake", "id": "FakeID"}).delete() + mock_requestbuilder_init.assert_called_with("fake") + + @mock.patch("exonetapi.RequestBuilder.__init__", return_value=None) + @mock.patch("exonetapi.RequestBuilder.post") + def test_post(self, mock_requestbuilder_post, mock_requestbuilder_init): + mock_requestbuilder_post.post = MagicMock(return_value=None) + ApiResource({"type": "fake", "id": "FakeID"}).post() + mock_requestbuilder_init.assert_called_with("fake") + + def test_reset_changed_attributes(self): + resource = ApiResource( + { + "type": "fake", + "id": "FakeID", + } + ) + resource.attribute("test", "Hello World") + + self.assertEqual( + {"attributes": {"test": "Hello World"}, "id": "FakeID", "type": "fake"}, + resource.to_json_changed_attributes(), + ) + + resource.reset_changed_attributes() + self.assertEqual({}, resource.to_json_changed_attributes()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/structures/test_api_resource_identifier.py b/tests/structures/test_api_resource_identifier.py new file mode 100644 index 0000000..5501ac5 --- /dev/null +++ b/tests/structures/test_api_resource_identifier.py @@ -0,0 +1,169 @@ +import unittest +from unittest import mock +from unittest.mock import MagicMock + +from tests.testCase import testCase + +from exonetapi.structures import ApiResourceIdentifier +from exonetapi.structures.ApiResource import ApiResource +from exonetapi.structures.Relationship import Relationship +from exonetapi.structures.Relation import Relation +from exonetapi import create_resource + +import json + + +class testApiResourceIdentifier(testCase): + def test_init(self): + resource = create_resource( + { + "type": "fake", + } + ) + resource.attribute("first_name", "John") + resource.attribute("last_name", "Doe") + + self.assertEqual(resource.attribute("first_name"), "John") + self.assertEqual(resource.type(), "fake") + self.assertIsNone(resource.id()) + self.assertEqual( + resource.attributes(), + { + "first_name": "John", + "last_name": "Doe", + }, + ) + + def test_get_relationship_create(self): + resource = create_resource( + { + "type": "fake", + } + ) + + relationship = resource.get_relationship("something") + self.assertIsInstance(relationship, Relationship) + + def test_set_relationship(self): + resource = create_resource({"type": "fake"}) + + resource.relationship( + "messages", + [ + create_resource({"type": "message", "id": "messageOne"}), + create_resource({"type": "message", "id": "messageTwo"}), + ], + ) + + self.assertEqual( + resource.get_json_relationships(), + { + "messages": { + "data": [ + {"id": "messageOne", "type": "message"}, + {"id": "messageTwo", "type": "message"}, + ] + } + }, + ) + + def test_get_json_relationships(self): + resource = create_resource({"type": "fake"}) + resource.relationship( + "ignored", + { + "data": { + "type": "this", + "id": "that", + } + }, + ) + resource.reset_changed_relations() + + resource.relationship("object", {"data": {"type": "this", "id": "that"}}) + resource.relationship("resource", ApiResource("this", "that")) + resource.relationship( + "resource_identifier", ApiResourceIdentifier("this", "that") + ) + resource.relationship("list", [ApiResourceIdentifier("this", "that")]) + + self.assertEqual( + resource.get_json_changed_relationships(), + { + "object": {"data": {"id": "that", "type": "this"}}, + "resource": {"data": {"id": "that", "type": "this"}}, + "resource_identifier": {"data": {"id": "that", "type": "this"}}, + "list": {"data": [{"id": "that", "type": "this"}]}, + }, + ) + + def test_get_json_changed_relationships(self): + resource = create_resource({"type": "fake"}) + + resource.relationship( + "messages", + { + "data": { + "type": "this", + "id": "that", + } + }, + ) + + self.assertEqual( + resource.get_json_changed_relationships(), + {"messages": {"data": {"id": "that", "type": "this"}}}, + ) + + def test_to_json(self): + resource = ApiResource( + { + "type": "fake", + "id": "FakeID", + } + ) + resource.set_relationship( + "thing", + create_resource( + { + "type": "things", + "id": "thingID", + } + ), + ) + + self.assertEqual( + json.dumps(resource.to_json()), + json.dumps( + { + "type": "fake", + "attributes": {}, + "id": "FakeID", + "relationships": { + "thing": {"data": {"type": "things", "id": "thingID"}} + }, + } + ), + ) + + def test_related(self): + resource = create_resource( + { + "type": "fake", + } + ) + + relation = resource.related("something") + self.assertIsInstance(relation, Relation) + + @mock.patch("exonetapi.RequestBuilder.__init__", return_value=None) + @mock.patch("exonetapi.RequestBuilder.get") + def test_post(self, mock_requestbuilder_get, mock_requestbuilder_init): + mock_requestbuilder_get.get = MagicMock(return_value=None) + ApiResource({"type": "fake", "id": "FakeID"}).get() + mock_requestbuilder_get.assert_called_with("FakeID") + mock_requestbuilder_init.assert_called_with("fake") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/structures/test_api_resource_set.py b/tests/structures/test_api_resource_set.py new file mode 100644 index 0000000..b882eac --- /dev/null +++ b/tests/structures/test_api_resource_set.py @@ -0,0 +1,69 @@ +import unittest +from unittest import mock + +from exonetapi.structures.ApiResourceSet import ApiResourceSet +from exonetapi.structures.ApiResource import ApiResource +from tests.testCase import testCase + + +class testApiResourceSet(testCase): + def test_total(self): + api_resource_set = ApiResourceSet() + meta = {"resources": {"total": 1337}} + api_resource_set.set_meta(meta) + + self.assertEqual(1337, api_resource_set.total()) + self.assertEqual(meta, api_resource_set.meta()) + + def test_links(self): + links = {"next": "next_link", "prev": "prev_link"} + api_resource_set = ApiResourceSet() + api_resource_set.set_links(links) + + self.assertEqual(links, api_resource_set.links()) + + @mock.patch("exonetapi.RequestBuilder.get", return_value="api_response") + def test_pagination(self, mock_request_builder): + links = { + "next": "https://api.exonet.nl/next_url?filter[unit]=test", + "prev": "https://api.exonet.nl/prev_url?filter[unit]=test", + "first": "https://api.exonet.nl/first/url?filter[unit]=test", + "last": "https://api.exonet.nl/last/url?filter[unit]=test&last=true", + } + api_resource_set = ApiResourceSet() + api_resource_set.set_links(links) + + self.assertEqual("api_response", api_resource_set.next_page()) + mock_request_builder.assert_called_with("next_url?filter[unit]=test") + + self.assertEqual("api_response", api_resource_set.previous_page()) + mock_request_builder.assert_called_with("prev_url?filter[unit]=test") + + self.assertEqual("api_response", api_resource_set.first_page()) + mock_request_builder.assert_called_with("first/url?filter[unit]=test") + + self.assertEqual("api_response", api_resource_set.last_page()) + mock_request_builder.assert_called_with("last/url?filter[unit]=test&last=true") + + api_resource_set.set_links({"next": None}) + self.assertEqual(None, api_resource_set.next_page()) + + def test_add_resource(self): + resource_one = ApiResource("fake", "abc") + resource_two = ApiResource("fake", "def") + + api_resource_set = ApiResourceSet() + api_resource_set.add_resource(resource_one) + api_resource_set.add_resource([resource_two]) + + self.assertEqual(2, len(api_resource_set)) + + for api_resource in api_resource_set: + self.assertTrue(api_resource.id() == "abc" or api_resource.id() == "def") + + for api_resource in api_resource_set.resources(): + self.assertTrue(api_resource.id() == "abc" or api_resource.id() == "def") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/structures/testRelation.py b/tests/structures/test_relation.py old mode 100755 new mode 100644 similarity index 52% rename from tests/structures/testRelation.py rename to tests/structures/test_relation.py index 3533eae..680fd78 --- a/tests/structures/testRelation.py +++ b/tests/structures/test_relation.py @@ -7,28 +7,26 @@ class testRelation(testCase): - def test_len_empty(self): - relation = Relation('author', 'posts', 'postID') + relation = Relation("author", "posts", "postID") self.assertEqual(0, len(relation)) def test_len_filled(self): - relation = Relation('author', 'posts', 'postID') - relation.set_resource_identifiers([ - 'a', 'b', 'c' - ]) + relation = Relation("author", "posts", "postID") + relation.set_resource_identifiers(["a", "b", "c"]) self.assertEqual(3, len(relation)) - @mock.patch('exonetapi.RequestBuilder.get', return_value='get_response') + @mock.patch("exonetapi.RequestBuilder.get", return_value="get_response") def test_getattr(self, mock_request_builder): - """Call a method on the relation and expect it to be passed to the RequestBuilder.""" - relation = Relation('author', 'posts', 'postID') + """Call a method on the relation and expect it to be passed to the + RequestBuilder.""" + relation = Relation("author", "posts", "postID") - self.assertEqual('get_response', relation.get()) + self.assertEqual("get_response", relation.get()) mock_request_builder.assert_called() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/testCase.py b/tests/testCase.py old mode 100755 new mode 100644 index cb6c01d..e87216a --- a/tests/testCase.py +++ b/tests/testCase.py @@ -3,7 +3,6 @@ class testCase(unittest.TestCase): - def setUp(self): # Reset Client singleton. Singleton._instances = {} diff --git a/tests/testClient.py b/tests/testClient.py deleted file mode 100755 index eaca947..0000000 --- a/tests/testClient.py +++ /dev/null @@ -1,35 +0,0 @@ -import unittest -from unittest import mock - -from tests.testCase import testCase -from exonetapi.Client import Client -from exonetapi.RequestBuilder import RequestBuilder - - -class testClient(testCase): - - @mock.patch('exonetapi.auth.Authenticator.__init__', return_value=None) - def test_init_arguments(self, mock_authenticator): - client = Client('https://test.url') - - self.assertEqual(client.get_host(), 'https://test.url') - mock_authenticator.assert_called_with('https://test.url', '/oauth/token') - - def test_set_host(self): - client = Client('https://test.url') - client.set_host('http://new.host') - self.assertEqual(client.get_host(), 'http://new.host') - - def test_set_host_invalid_protocol(self): - client = Client('https://test.url') - self.assertRaises(ConnectionAbortedError, client.set_host, 'ftp://new.host') - - def test_resource(self): - client = Client('https://test.url') - resource = client.resource('/test') - - self.assertIsInstance(resource, RequestBuilder) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/testRequestBuilder.py b/tests/testRequestBuilder.py deleted file mode 100755 index 311572a..0000000 --- a/tests/testRequestBuilder.py +++ /dev/null @@ -1,318 +0,0 @@ -import unittest -from unittest.mock import MagicMock -from unittest import mock - -from requests import Response - -from tests.testCase import testCase -from exonetapi import Client -from exonetapi.RequestBuilder import RequestBuilder -from exonetapi.structures.ApiResource import ApiResource -from exonetapi.exceptions.ValidationException import ValidationException - - -class testRequestBuilder(testCase): - def setUp(self): - super().setUp() - client = Client('https://api.exonet.nl') - self.request_builder = RequestBuilder('things', client) - - def tearDown(self): - super().tearDown() - self.request_builder = None - - class MockResponse: - def __init__(self, content, status_code=200): - self.content = content - self.status_code = status_code - - def raise_for_status(self): - return None - - def test_init_arguments(self): - self.assertEqual(self.request_builder._RequestBuilder__resource, '/things') - - def test_filter(self): - self.request_builder.filter('firstFilterName', 'firstFilterValue') - self.request_builder.filter('secondFilterName', 'secondFilterValue') - self.assertEqual( - self.request_builder._RequestBuilder__query_params['filter[firstFilterName]'], - 'firstFilterValue' - ) - self.assertEqual( - self.request_builder._RequestBuilder__query_params['filter[secondFilterName]'], - 'secondFilterValue' - ) - - def test_page(self): - self.request_builder.page(3) - self.assertEqual(self.request_builder._RequestBuilder__query_params['page[number]'], 3) - - def test_size(self): - self.request_builder.size(30) - self.assertEqual(self.request_builder._RequestBuilder__query_params['page[size]'], 30) - - def test_sort_default(self): - self.request_builder.sort('domain') - self.assertEqual(self.request_builder._RequestBuilder__query_params['sort'], 'domain') - - def test_sort_invalid(self): - self.assertRaises(ValueError, self.request_builder.sort, 'domain', 'topdown') - - def test_sortAsc(self): - self.request_builder.sort_asc('domain') - self.assertEqual(self.request_builder._RequestBuilder__query_params['sort'], 'domain') - - def test_sortDesc(self): - self.request_builder.sort_desc('domain') - self.assertEqual(self.request_builder._RequestBuilder__query_params['sort'], '-domain') - - @mock.patch('exonetapi.auth.Authenticator.get_token') - def test_get_headers(self, mock_authenticator_get_token): - mock_authenticator_get_token.return_value = 'test_token' - - headers = self.request_builder._RequestBuilder__get_headers() - self.assertEqual(headers, { - 'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer test_token' - }) - - @mock.patch('exonetapi.result.Parser.parse') - @mock.patch('exonetapi.result.Parser.__init__') - @mock.patch('requests.request') - def test_get(self, mock_requests_request, mock_parser_init, mock_parser_parse): - mock_parser_parse.return_value = 'parsedReturnValue' - mock_parser_init.return_value = None - mock_requests_request.return_value = self.MockResponse('{"data": "getReturnData"}') - - result = self.request_builder.get('testIad') - - mock_requests_request.assert_called_with( - 'GET', - 'https://api.exonet.nl/things/testIad', - headers={ - 'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer None' - }, - params=None, - json=None) - - mock_parser_init.assert_called_with('{"data": "getReturnData"}') - - self.assertTrue(mock_parser_parse.called) - self.assertEqual('parsedReturnValue', result) - - @mock.patch('exonetapi.result.Parser.parse') - @mock.patch('exonetapi.result.Parser.__init__') - @mock.patch('requests.request') - def test_post(self, mock_requests_request, mock_parser_init, mock_parser_parse): - resource = ApiResource({'type': 'things', 'id': 'someId'}) - resource.to_json = MagicMock(return_value={'name': 'my_name'}) - resource.to_json_changed_attributes = MagicMock(return_value={'name': 'my_name'}) - - mock_parser_parse.return_value = 'parsedReturnValue' - mock_parser_init.return_value = None - mock_requests_request.return_value = self.MockResponse('{"data": "getReturnData"}') - - result = self.request_builder.post(resource) - - mock_requests_request.assert_called_with( - 'POST', - 'https://api.exonet.nl/things', - headers={ - 'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer None' - }, - json={'data': {'name': 'my_name'}}, - params=None) - - mock_parser_init.assert_called_with('{"data": "getReturnData"}') - - self.assertTrue(mock_parser_parse.called) - self.assertEqual('parsedReturnValue', result) - - @mock.patch('exonetapi.result.Parser.parse') - @mock.patch('exonetapi.result.Parser.__init__') - @mock.patch('requests.request') - def test_post_relation(self, mock_requests_request, mock_parser_init, mock_parser_parse): - resource = ApiResource({'type': 'things', 'id': 'someId'}) - resource.get_json_changed_relationships = MagicMock( - return_value={'name': {'data': {'type': 'test', 'id': 1}}} - ) - - mock_parser_parse.return_value = 'parsedReturnValue' - mock_parser_init.return_value = None - mock_requests_request.return_value = self.MockResponse('{"data": "getReturnData"}') - - result = self.request_builder.post(resource) - - mock_requests_request.assert_called_with( - 'POST', - 'https://api.exonet.nl/things/someId/relationships/name', - headers={ - 'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer None' - }, - json={'data': {'type': 'test', 'id': 1}}, - params=None) - - mock_parser_init.assert_called_with('{"data": "getReturnData"}') - - self.assertTrue(mock_parser_parse.called) - self.assertEqual(['parsedReturnValue'], result) - - @mock.patch('requests.request') - def test_patch(self, mock_requests_request): - resource = ApiResource({'type': 'things', 'id': 'someId'}) - resource.to_json = MagicMock(return_value={'name': 'my_name'}) - resource.to_json_changed_attributes = MagicMock(return_value={'name': 'my_name'}) - - mock_requests_request.return_value = self.MockResponse('{"data": "getReturnData"}') - - result = self.request_builder.patch(resource) - - mock_requests_request.assert_called_with( - 'PATCH', - 'https://api.exonet.nl/things/someId', - headers={ - 'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer None' - }, - json={'data': {'name': 'my_name'}}, - params=None) - - self.assertTrue(result) - - @mock.patch('requests.request') - def test_patch_relation(self, mock_requests_request): - resource = ApiResource({'type': 'things', 'id': 'someId'}) - resource.get_json_changed_relationships = MagicMock( - return_value={'name': {'data': {'type': 'test', 'id': 1}}} - ) - - mock_requests_request.return_value = self.MockResponse('{"data": "getReturnData"}') - - result = self.request_builder.patch(resource) - - mock_requests_request.assert_called_with( - 'PATCH', - 'https://api.exonet.nl/things/someId/relationships/name', - headers={ - 'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer None' - }, - json={'data': {'type': 'test', 'id': 1}}, - params=None) - - self.assertTrue(result) - - @mock.patch('requests.request') - def test_delete(self, mock_requests_request): - resource = ApiResource({'type': 'things', 'id': 'someId'}) - resource.to_json = MagicMock(return_value={'name': 'my_name'}) - resource.to_json_changed_attributes = MagicMock(return_value={'name': 'my_name'}) - - mock_requests_request.return_value = self.MockResponse('{"data": "getReturnData"}') - - result = self.request_builder.delete(resource) - - mock_requests_request.assert_called_with( - 'DELETE', - 'https://api.exonet.nl/things/someId', - headers={ - 'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer None' - }, - json=None, - params=None) - - self.assertTrue(result) - - @mock.patch('requests.request') - def test_delete_relation(self, mock_requests_request): - resource = ApiResource({'type': 'things', 'id': 'someId'}) - resource.get_json_changed_relationships = MagicMock( - return_value={'name': {'data': {'type': 'test', 'id': 1}}}) - - mock_requests_request.return_value = self.MockResponse('{"data": "getReturnData"}') - - result = self.request_builder.delete(resource) - - mock_requests_request.assert_called_with( - 'DELETE', - 'https://api.exonet.nl/things/someId/relationships/name', - headers={ - 'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer None' - }, - json={'data': {'type': 'test', 'id': 1}}, - params=None) - - self.assertTrue(result) - - @mock.patch('requests.request') - @mock.patch('exonetapi.exceptions.ValidationException.__init__', return_value=None) - def test_post_validation_error(self, mock_validation_exception, mock_requests_request): - resource = ApiResource({'type': 'things', 'id': 'someId'}) - resource.to_json = MagicMock(return_value={'name': 'my_name'}) - resource.to_json_changed_attributes = MagicMock(return_value={'name': 'my_name'}) - - mock_requests_request.return_value = self.MockResponse('{"data": "getReturnData"}', 422) - - self.assertRaises(ValidationException, self.request_builder.post, resource) - - mock_requests_request.assert_called_with( - 'POST', - 'https://api.exonet.nl/things', - headers={ - 'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer None' - }, - json={'data': {'name': 'my_name'}}, - params=None) - - @mock.patch('requests.request') - def test_get_recursive(self, mock_requests_request): - result_one = Response() - result_one.status_code = 200 - result_one._content = str.encode('{"data": ' \ - '[{"type": "test", "id": "abc"}], ' \ - '"meta": {"total": 2}, ' \ - '"links": {"next": "https://api.exonet.nl/next_page"}' \ - '}') - - result_two = Response() - result_two.status_code = 200 - result_two._content = str.encode('{"data": ' \ - '[{"type": "test", "id": "def"}], ' \ - '"meta": {"total": 2}, ' \ - '"links": {"next": null}' \ - '}') - - request_result = [result_one, result_two] - mock_requests_request.side_effect = request_result - - self.request_builder.get_recursive() - mock_requests_request.assert_any_call('GET', 'https://api.exonet.nl/things', - headers={'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer None'}, json=None, - params={}) - mock_requests_request.assert_any_call('GET', 'https://api.exonet.nl/next_page', - headers={'Accept': 'application/vnd.Exonet.v1+json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer None'}, json=None, - params=None) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..8c30b7a --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,34 @@ +import unittest +from unittest import mock + +from tests.testCase import testCase +from exonetapi.Client import Client +from exonetapi.RequestBuilder import RequestBuilder + + +class testClient(testCase): + @mock.patch("exonetapi.auth.Authenticator.__init__", return_value=None) + def test_init_arguments(self, mock_authenticator): + client = Client("https://test.url") + + self.assertEqual(client.get_host(), "https://test.url") + mock_authenticator.assert_called_with("https://test.url", "/oauth/token") + + def test_set_host(self): + client = Client("https://test.url") + client.set_host("http://new.host") + self.assertEqual(client.get_host(), "http://new.host") + + def test_set_host_invalid_protocol(self): + client = Client("https://test.url") + self.assertRaises(ConnectionAbortedError, client.set_host, "ftp://new.host") + + def test_resource(self): + client = Client("https://test.url") + resource = client.resource("/test") + + self.assertIsInstance(resource, RequestBuilder) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_create_resource.py b/tests/test_create_resource.py old mode 100755 new mode 100644 index e0d77e3..5e0db75 --- a/tests/test_create_resource.py +++ b/tests/test_create_resource.py @@ -7,14 +7,12 @@ class test_create_resource(testCase): def test_create_resource(self): - resource_data = { - 'type': 'test_resource' - } + resource_data = {"type": "test_resource"} resource = create_resource(resource_data) - self.assertEqual(resource.__class__.__name__, 'TestResource') + self.assertEqual(resource.__class__.__name__, "TestResource") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_request_builder.py b/tests/test_request_builder.py new file mode 100644 index 0000000..54003d3 --- /dev/null +++ b/tests/test_request_builder.py @@ -0,0 +1,388 @@ +import unittest +from unittest.mock import MagicMock +from unittest import mock + +from requests import Response + +from tests.testCase import testCase +from exonetapi import Client +from exonetapi.RequestBuilder import RequestBuilder +from exonetapi.structures.ApiResource import ApiResource +from exonetapi.exceptions.ValidationException import ValidationException + + +class testRequestBuilder(testCase): + def setUp(self): + super().setUp() + client = Client("https://api.exonet.nl") + self.request_builder = RequestBuilder("things", client) + + def tearDown(self): + super().tearDown() + self.request_builder = None + + class MockResponse: + def __init__(self, content, status_code=200): + self.content = content + self.status_code = status_code + + def raise_for_status(self): + return None + + def test_init_arguments(self): + self.assertEqual(self.request_builder._RequestBuilder__resource, "/things") + + def test_filter(self): + self.request_builder.filter("firstFilterName", "firstFilterValue") + self.request_builder.filter("secondFilterName", "secondFilterValue") + self.assertEqual( + self.request_builder._RequestBuilder__query_params[ + "filter[firstFilterName]" + ], + "firstFilterValue", + ) + self.assertEqual( + self.request_builder._RequestBuilder__query_params[ + "filter[secondFilterName]" + ], + "secondFilterValue", + ) + + def test_page(self): + self.request_builder.page(3) + self.assertEqual( + self.request_builder._RequestBuilder__query_params["page[number]"], 3 + ) + + def test_size(self): + self.request_builder.size(30) + self.assertEqual( + self.request_builder._RequestBuilder__query_params["page[size]"], 30 + ) + + def test_sort_default(self): + self.request_builder.sort("domain") + self.assertEqual( + self.request_builder._RequestBuilder__query_params["sort"], "domain" + ) + + def test_sort_invalid(self): + self.assertRaises(ValueError, self.request_builder.sort, "domain", "topdown") + + def test_sortAsc(self): + self.request_builder.sort_asc("domain") + self.assertEqual( + self.request_builder._RequestBuilder__query_params["sort"], "domain" + ) + + def test_sortDesc(self): + self.request_builder.sort_desc("domain") + self.assertEqual( + self.request_builder._RequestBuilder__query_params["sort"], "-domain" + ) + + @mock.patch("exonetapi.auth.Authenticator.get_token") + def test_get_headers(self, mock_authenticator_get_token): + mock_authenticator_get_token.return_value = "test_token" + + headers = self.request_builder._RequestBuilder__get_headers() + self.assertEqual( + headers, + { + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer test_token", + }, + ) + + @mock.patch("exonetapi.result.Parser.parse") + @mock.patch("exonetapi.result.Parser.__init__") + @mock.patch("requests.request") + def test_get(self, mock_requests_request, mock_parser_init, mock_parser_parse): + mock_parser_parse.return_value = "parsedReturnValue" + mock_parser_init.return_value = None + mock_requests_request.return_value = self.MockResponse( + '{"data": "getReturnData"}' + ) + + result = self.request_builder.get("testIad") + + mock_requests_request.assert_called_with( + "GET", + "https://api.exonet.nl/things/testIad", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer None", + }, + params=None, + json=None, + ) + + mock_parser_init.assert_called_with('{"data": "getReturnData"}') + + self.assertTrue(mock_parser_parse.called) + self.assertEqual("parsedReturnValue", result) + + @mock.patch("exonetapi.result.Parser.parse") + @mock.patch("exonetapi.result.Parser.__init__") + @mock.patch("requests.request") + def test_post(self, mock_requests_request, mock_parser_init, mock_parser_parse): + resource = ApiResource({"type": "things", "id": "someId"}) + resource.to_json = MagicMock(return_value={"name": "my_name"}) + resource.to_json_changed_attributes = MagicMock( + return_value={"name": "my_name"} + ) + + mock_parser_parse.return_value = "parsedReturnValue" + mock_parser_init.return_value = None + mock_requests_request.return_value = self.MockResponse( + '{"data": "getReturnData"}' + ) + + result = self.request_builder.post(resource) + + mock_requests_request.assert_called_with( + "POST", + "https://api.exonet.nl/things", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer None", + }, + json={"data": {"name": "my_name"}}, + params=None, + ) + + mock_parser_init.assert_called_with('{"data": "getReturnData"}') + + self.assertTrue(mock_parser_parse.called) + self.assertEqual("parsedReturnValue", result) + + @mock.patch("exonetapi.result.Parser.parse") + @mock.patch("exonetapi.result.Parser.__init__") + @mock.patch("requests.request") + def test_post_relation( + self, mock_requests_request, mock_parser_init, mock_parser_parse + ): + resource = ApiResource({"type": "things", "id": "someId"}) + resource.get_json_changed_relationships = MagicMock( + return_value={"name": {"data": {"type": "test", "id": 1}}} + ) + + mock_parser_parse.return_value = "parsedReturnValue" + mock_parser_init.return_value = None + mock_requests_request.return_value = self.MockResponse( + '{"data": "getReturnData"}' + ) + + result = self.request_builder.post(resource) + + mock_requests_request.assert_called_with( + "POST", + "https://api.exonet.nl/things/someId/relationships/name", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer None", + }, + json={"data": {"type": "test", "id": 1}}, + params=None, + ) + + mock_parser_init.assert_called_with('{"data": "getReturnData"}') + + self.assertTrue(mock_parser_parse.called) + self.assertEqual(["parsedReturnValue"], result) + + @mock.patch("requests.request") + def test_patch(self, mock_requests_request): + resource = ApiResource({"type": "things", "id": "someId"}) + resource.to_json = MagicMock(return_value={"name": "my_name"}) + resource.to_json_changed_attributes = MagicMock( + return_value={"name": "my_name"} + ) + + mock_requests_request.return_value = self.MockResponse( + '{"data": "getReturnData"}' + ) + + result = self.request_builder.patch(resource) + + mock_requests_request.assert_called_with( + "PATCH", + "https://api.exonet.nl/things/someId", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer None", + }, + json={"data": {"name": "my_name"}}, + params=None, + ) + + self.assertTrue(result) + + @mock.patch("requests.request") + def test_patch_relation(self, mock_requests_request): + resource = ApiResource({"type": "things", "id": "someId"}) + resource.get_json_changed_relationships = MagicMock( + return_value={"name": {"data": {"type": "test", "id": 1}}} + ) + + mock_requests_request.return_value = self.MockResponse( + '{"data": "getReturnData"}' + ) + + result = self.request_builder.patch(resource) + + mock_requests_request.assert_called_with( + "PATCH", + "https://api.exonet.nl/things/someId/relationships/name", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer None", + }, + json={"data": {"type": "test", "id": 1}}, + params=None, + ) + + self.assertTrue(result) + + @mock.patch("requests.request") + def test_delete(self, mock_requests_request): + resource = ApiResource({"type": "things", "id": "someId"}) + resource.to_json = MagicMock(return_value={"name": "my_name"}) + resource.to_json_changed_attributes = MagicMock( + return_value={"name": "my_name"} + ) + + mock_requests_request.return_value = self.MockResponse( + '{"data": "getReturnData"}' + ) + + result = self.request_builder.delete(resource) + + mock_requests_request.assert_called_with( + "DELETE", + "https://api.exonet.nl/things/someId", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer None", + }, + json=None, + params=None, + ) + + self.assertTrue(result) + + @mock.patch("requests.request") + def test_delete_relation(self, mock_requests_request): + resource = ApiResource({"type": "things", "id": "someId"}) + resource.get_json_changed_relationships = MagicMock( + return_value={"name": {"data": {"type": "test", "id": 1}}} + ) + + mock_requests_request.return_value = self.MockResponse( + '{"data": "getReturnData"}' + ) + + result = self.request_builder.delete(resource) + + mock_requests_request.assert_called_with( + "DELETE", + "https://api.exonet.nl/things/someId/relationships/name", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer None", + }, + json={"data": {"type": "test", "id": 1}}, + params=None, + ) + + self.assertTrue(result) + + @mock.patch("requests.request") + @mock.patch("exonetapi.exceptions.ValidationException.__init__", return_value=None) + def test_post_validation_error( + self, mock_validation_exception, mock_requests_request + ): + resource = ApiResource({"type": "things", "id": "someId"}) + resource.to_json = MagicMock(return_value={"name": "my_name"}) + resource.to_json_changed_attributes = MagicMock( + return_value={"name": "my_name"} + ) + + mock_requests_request.return_value = self.MockResponse( + '{"data": "getReturnData"}', 422 + ) + + self.assertRaises(ValidationException, self.request_builder.post, resource) + + mock_requests_request.assert_called_with( + "POST", + "https://api.exonet.nl/things", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer None", + }, + json={"data": {"name": "my_name"}}, + params=None, + ) + + @mock.patch("requests.request") + def test_get_recursive(self, mock_requests_request): + result_one = Response() + result_one.status_code = 200 + result_one._content = str.encode( + '{"data": ' + '[{"type": "test", "id": "abc"}], ' + '"meta": {"total": 2}, ' + '"links": {"next": "https://api.exonet.nl/next_page"}' + "}" + ) + + result_two = Response() + result_two.status_code = 200 + result_two._content = str.encode( + '{"data": ' + '[{"type": "test", "id": "def"}], ' + '"meta": {"total": 2}, ' + '"links": {"next": null}' + "}" + ) + + request_result = [result_one, result_two] + mock_requests_request.side_effect = request_result + + self.request_builder.get_recursive() + mock_requests_request.assert_any_call( + "GET", + "https://api.exonet.nl/things", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer None", + }, + json=None, + params={}, + ) + mock_requests_request.assert_any_call( + "GET", + "https://api.exonet.nl/next_page", + headers={ + "Accept": "application/vnd.Exonet.v1+json", + "Content-Type": "application/json", + "Authorization": "Bearer None", + }, + json=None, + params=None, + ) + + +if __name__ == "__main__": + unittest.main()