diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4efa1cf..76e4fbe 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,7 @@ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: @@ -9,18 +9,10 @@ updates: directory: "/" schedule: interval: "weekly" - day: "tuesday" - commit-message: - prefix: "[FIX]" - prefix-development: "[CHORE]" - include: scope - # Fetch and update latest `github-actions` pkgs - - package-ecosystem: github-actions - directory: '/' - schedule: - interval: "weekly" - day: "tuesday" - commit-message: - prefix: "[FIX]" - prefix-development: "[CHORE]" - include: scope + groups: + minor-and-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..a8e58e4 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,17 @@ +name: pre-commit + +on: + push: + branches: + - "master" + pull_request: + branches: + - "*" + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8f5462c..01b896f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,44 +10,23 @@ on: jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest name: Run tests steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: '3.6' - cache: 'pip' + python-version: "3.13" + cache: "pip" - name: Install Dependencies - run: pip install -r requirements.txt -r requirements-dev.txt + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt - name: Run tests run: | make development source virtualenv_run/bin/activate make test - - security: - runs-on: ubuntu-20.04 - name: Run style/security checks - - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-python@v4 - with: - python-version: '3.6' - cache: 'pip' - - - name: Install Dependencies - run: pip install -r requirements.txt -r requirements-dev.txt - - - name: Safety Check - shell: bash - run: | - make development - source virtualenv_run/bin/activate - pip install -U safety - safety check -i 44610 -i 51499 -i 51457 -i 39253 -i 44634 -i 50473 -i 52495 -i 53269 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4d6c21a..aae46ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,54 +1,25 @@ repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + - repo: https://gitlab.com/vojko.pribudic.foss/pre-commit-update + rev: v0.8.0 hooks: - - id: trailing-whitespace - language_version: python3 - - id: end-of-file-fixer - language_version: python3 - - id: check-merge-conflict - language_version: python3 - - id: requirements-txt-fixer - language_version: python3 - - id: name-tests-test - language_version: python3 - - id: double-quote-string-fixer - language_version: python3 - - id: forbid-new-submodules - language_version: python3 - - id: check-yaml - language_version: python3 - files: (\.(yaml|yml|eyaml))$ - - id: check-json - files: \.(jshintrc|json)$ -- repo: https://github.com/pycqa/flake8 - rev: '' + - id: pre-commit-update + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 hooks: - - id: flake8 - language_version: python3 - args: [ - --max-line-length=100, - # We did some funky thing in __init__.py, skip them for now. - --exclude=*__init__.py - ] -- repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.1 + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: detect-private-key + - id: requirements-txt-fixer + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 hooks: - - id: autopep8 - language_version: python3 -- repo: https://github.com/asottile/add-trailing-comma - rev: v2.0.1 + - id: codespell + exclude: ^locale/ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.5 hooks: - - id: add-trailing-comma -- repo: https://github.com/asottile/reorder_python_imports - rev: v2.2.0 - hooks: - - id: reorder-python-imports - language_version: python3 -- repo: https://github.com/asottile/pyupgrade - rev: v2.1.1 - hooks: - - id: pyupgrade - args: - - --py3-plus - language_version: python3 + - id: ruff-check + args: [--fix] + - id: ruff-format diff --git a/CHANGELOG.md b/CHANGELOG.md index 8efed7e..1cf8809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,24 @@ -## Upcoming Changes +## `2.0.0` - 2025-07-24 -#### Fixed +#### 🚀 Major Changes -- ... +- **Python Version Support**: Updated minimum Python version to 3.11+ + - Dropped support for Python 3.10 and below + - Development environment uses Python 3.13 -#### Changed +#### 📦 Dependencies -- ... +- Updated core dependencies: `web3` (6→7.12.1), `websockets` (10.0→15.0.1), `requests` (2.32.4) +- Updated development dependencies: `pre-commit` (3.7.0→4.2.0), `pytest` (8.4.1), `coverage` (7.9.2) -#### Added +#### 🔧 Improvements -- ... - -## `1.2.0` - 04/16/2024 - -#### Changed - -- Support for up to python 3.11: - - Bump web3 dependencies, bump some dev dependencies. - - Replace deprecated eth_account methods on the account recovery. - - Bump websockets to 10.0 to support python 3.10+ +- Fixed pre-commit hook compatibility issues +- Improved test coverage and multi-version testing +- Enhanced documentation and development workflow +- Updated all configuration files for Python 3.11+ support +--- ## `1.0.0` - 07/05/2023 @@ -30,14 +28,12 @@ - [Security Enhancement]: Validate `aud` using Magic client ID. - Pull client ID from Magic servers if not provided in constructor. - ## `0.3.3` - 05/02/2023 #### Changed - PR-#77: Removing NFT functionality, clients will interact with the NFT API directly via API calls. - ## `0.3.2` - 03/21/2023 #### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc18629..93f9681 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Please note we have a **code of conduct**, please follow it in all your interact ## Setting up for Local Development -1. Fork this repostiory. +1. Fork this repository. 2. Clone your fork. 3. Create a new branch in your local repository with the following pattern: @@ -126,7 +126,7 @@ Violating these terms may lead to a permanent ban. #### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/Makefile b/Makefile index 6ed17aa..deb63f8 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ development: virtualenv_run install_prod_requirements install_dev_requirements i .PHONY: virtualenv_run virtualenv_run: - virtualenv -p python3.11 virtualenv_run + virtualenv -p python3.13 virtualenv_run virtualenv_run/bin/pip install --upgrade pip .PHONY: install_prod_requirements diff --git a/README.md b/README.md index d07eb63..75cc2c1 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,153 @@ # Magic Admin Python SDK -The Magic Admin Python SDK provides convenient ways for developers to interact with Magic API endpoints and an array of utilities to handle [DID Token](https://magic.link/docs/auth/introduction/decentralized-id). +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![PyPI version](https://badge.fury.io/py/magic-admin.svg)](https://badge.fury.io/py/magic-admin) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE.txt) -## Table of Contents +The Magic Admin Python SDK provides a simple and powerful way to integrate Magic's authentication system into your Python applications. Handle [DID Tokens](https://magic.link/docs/auth/introduction/decentralized-id) and interact with Magic API endpoints with ease. -* [Documentation](#documentation) -* [Quick Start](#quick-start) -* [Development](#development) -* [Changelog](#changelog) -* [License](#license) +## 📚 Documentation -## Documentation -See the [Magic doc](https://magic.link/docs/auth/api-reference/server-side-sdks/python)! +📖 **Full Documentation**: [Magic Python SDK Docs](https://magic.link/docs/auth/api-reference/server-side-sdks/python) -## Installation -You can directly install the SDK with: +## 🚀 Quick Start -pip: +### Installation -``` +```bash +# Using pip pip install magic-admin -``` - -conda: -``` +# Using conda conda install magic-admin ``` -### Prerequisites +### Basic Usage -- Python 3.6 - -**Note**: This package has only been tested with `Python 3.6`. `Python 3.7` and `Python 3.8` have not been tested yet. We will get to it very soon. Support for `Python 2.7+` will not be actively worked on. If you are interested using this package with earlier versions of Python, please create a ticket and let us know :) +```python +from magic_admin import Magic -## Quick Start -Before you start, you will need an API secret key. You can get one from the [Magic Dashboard](https://dashboard.magic.link/). Once you have the API secret key, you can instantiate a Magic object. +# Initialize with your API secret key +magic = Magic(api_secret_key='your_api_secret_key_here') +# Validate a DID token +try: + magic.Token.validate('DID_TOKEN_FROM_CLIENT') + print("Token is valid!") +except Exception as e: + print(f"Token validation failed: {e}") ``` -from magic_admin import Magic -magic = Magic(api_secret_key='') +### Environment Variable Configuration -magic.Token.validate('DID_TOKEN') +You can also load your API secret key from an environment variable: -# Read the docs to learn more! 🚀 +```bash +export MAGIC_API_SECRET_KEY="your_api_secret_key_here" ``` -Optionally if you would like, you can load the API secret key from the environment variable, `MAGIC_API_SECRET_KEY`. - -``` -# Set the env variable `MAGIC_API_SECRET_KEY`. +```python +from magic_admin import Magic +# Automatically uses MAGIC_API_SECRET_KEY environment variable magic = Magic() ``` -**Note**: The argument passed to the `Magic(...)` object takes precedence over the environment variable. +> **Note**: The API secret key passed directly to `Magic()` takes precedence over the environment variable. -### Configure Network Strategy -The `Magic` object also takes in `retries`, `timeout` and `backoff_factor` as optional arguments at the object instantiation time so you can override those values for your application setup. +### Network Configuration -``` -magic = Magic(retries=5, timeout=10, backoff_factor=0.03) +Customize network behavior for your application: + +```python +magic = Magic( + api_secret_key='your_key', + retries=5, # Number of retry attempts + timeout=10, # Request timeout in seconds + backoff_factor=0.03 # Exponential backoff factor +) ``` -## Development -We would love to have you contributing to this SDK. To get started, you can clone this repository and create a virtualenv. +## 🔧 Development -``` -make development -``` +### Prerequisites -This will create a virtualenv for all the local development dependencies that the SDK will needs. +- Python 3.11+ +- Git -Once it is done, you can `source` the virtualenv. It makes your local development easier! +### Setup Development Environment -``` +```bash +# Clone the repository +git clone https://github.com/magiclabs/magic-admin-python.git +cd magic-admin-python + +# Create virtual environment and install dependencies +make development + +# Activate the virtual environment source virtualenv_run/bin/activate ``` -To make sure your new code works with the existing SDK, run the test against the current supported Python versions. +### Running Tests -``` +```bash +# Run tests against all supported Python versions (3.11, 3.12, 3.13) +make test + +# Run tests with coverage make test ``` -To clean up existing virtualenv, tox log and pytest cache, do a +### Code Quality + +This project uses [pre-commit](https://pre-commit.com/) to maintain code quality. Hooks run automatically on every commit. +```bash +# Run pre-commit hooks manually +pre-commit run --all-files + +# Install pre-commit hooks +pre-commit install ``` + +### Cleanup + +```bash +# Remove virtual environment, tox logs, and pytest cache make clean ``` -This repository is installed with [pre-commit](https://pre-commit.com/). All of the pre-commit hooks are run automatically with every new commit. This is to keep the codebase styling and format consistent. +## 📋 Requirements -You can also run the pre-commit manually. You can find all the pre-commit hooks [here](.pre-commit-config.yaml). +- **Python**: 3.11+ +- **Dependencies**: See [requirements.txt](requirements.txt) +- **Development**: See [requirements-dev.txt](requirements-dev.txt) -``` -pre-commit run -``` +## 🤝 Contributing + +We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. + +### Development Workflow + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Run tests: `make test` +5. Run pre-commit: `pre-commit run --all-files` +6. Submit a pull request + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE.txt](LICENSE.txt) file for details. + +## 📝 Changelog -Please also see our [CONTRIBUTING](CONTRIBUTING.md) guide for other information. +See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes. -## Changelog -See [Changelog](CHANGELOG.md) +## 🔗 Links -## License -See [License](LICENSE.txt) +- [Magic Documentation](https://magic.link/docs) +- [Magic Dashboard](https://dashboard.magic.link/) +- [Magic Python SDK Docs](https://magic.link/docs/auth/api-reference/server-side-sdks/python) +- [DID Token Documentation](https://magic.link/docs/auth/introduction/decentralized-id) diff --git a/magic_admin/__init__.py b/magic_admin/__init__.py index 04de948..5e343f4 100644 --- a/magic_admin/__init__.py +++ b/magic_admin/__init__.py @@ -1,4 +1,4 @@ -from magic_admin.magic import Magic +from magic_admin.magic import Magic # noqa: F401 # Magic API secret key. diff --git a/magic_admin/config.py b/magic_admin/config.py index ce0a8ec..6cdb2e9 100644 --- a/magic_admin/config.py +++ b/magic_admin/config.py @@ -1,7 +1,9 @@ -base_url = 'https://api.magic.link' +base_url = "https://api.toaster.magic.link" -api_secret_api_key_missing_message = 'API secret key is missing. Please specify ' \ - 'an API secret key when you instantiate the `Magic(api_secret_key=)` ' \ - 'object or use the environment variable, `MAGIC_API_SECRET_KEY`. You can ' \ - 'get your API secret key from https://dashboard.magic.link. If you are having ' \ - 'trouble, please don\'t hesitate to reach out to us at support@magic.link' +api_secret_api_key_missing_message = ( + "API secret key is missing. Please specify " + "an API secret key when you instantiate the `Magic(api_secret_key=)` " + "object or use the environment variable, `MAGIC_API_SECRET_KEY`. You can " + "get your API secret key from https://dashboard.magic.link. If you are having " + "trouble, please don't hesitate to reach out to us at support@magic.link" +) diff --git a/magic_admin/error.py b/magic_admin/error.py index e58b041..4b9d0d0 100644 --- a/magic_admin/error.py +++ b/magic_admin/error.py @@ -1,20 +1,19 @@ class MagicError(Exception): - def __init__(self, message=None): super().__init__(message) self._message = message def __str__(self): - return self._message or '' + return self._message or "" def __repr__(self): - return '{error_class}(message={message!r})'.format( + return "{error_class}(message={message!r})".format( error_class=self.__class__.__name__, message=self._message, ) def to_dict(self): - return {'message': str(self)} + return {"message": str(self)} class DIDTokenInvalid(MagicError): @@ -34,7 +33,6 @@ class APIConnectionError(MagicError): class RequestError(MagicError): - def __init__( self, message=None, @@ -58,19 +56,21 @@ def __init__( self.http_method = http_method def __repr__(self): - return '{error_class}(message={message!r}, ' \ - 'http_error_code={http_error_code}, ' \ - 'http_code={http_code}).'.format( + return ( + "{error_class}(message={message!r}, " + "http_error_code={http_error_code}, " + "http_code={http_code}).".format( error_class=self.__class__.__name__, message=self._message or None, http_error_code=self.http_error_code or None, http_code=self.http_code or None, ) + ) def to_dict(self): _dict = super().to_dict() for attr in self.__dict__: - if attr.startswith('http_'): + if attr.startswith("http_"): _dict[attr] = self.__dict__[attr] return _dict diff --git a/magic_admin/http_client.py b/magic_admin/http_client.py index 78809b5..7f3df50 100644 --- a/magic_admin/http_client.py +++ b/magic_admin/http_client.py @@ -19,7 +19,6 @@ class RequestsClient: - def __init__(self, retries, timeout, backoff_factor): self._retries = retries self._timeout = timeout @@ -32,21 +31,21 @@ def _get_platform_info(): platform_info = {} for attr, func in [ - ['platform', platform.platform], - ['language_version', platform.python_version], - ['uname', platform.uname], + ["platform", platform.platform], + ["language_version", platform.python_version], + ["uname", platform.uname], ]: try: val = str(func()) except Exception as e: - val = '<{}>'.format(str(e)) + val = "<{}>".format(str(e)) platform_info[attr] = val return platform_info def _setup_request_session(self): - """Take advantage of the ``requets.Session``. If client is making several + """Take advantage of the ``requests.Session``. If client is making several requests to the same host, the underlying TCP connection will be reused, which can result in a significant performance increase. """ @@ -63,10 +62,10 @@ def _setup_request_session(self): def _get_request_headers(self): user_agent = { - 'language': 'python', - 'sdk_version': version.VERSION, - 'publisher': 'magic', - 'http_lib': self.__class__.__name__, + "language": "python", + "sdk_version": version.VERSION, + "publisher": "magic", + "http_lib": self.__class__.__name__, **self._get_platform_info(), } @@ -74,8 +73,8 @@ def _get_request_headers(self): raise AuthenticationError(api_secret_api_key_missing_message) return { - 'X-Magic-Secret-Key': magic_admin.api_secret_key, - 'User-Agent': json.dumps(user_agent), + "X-Magic-Secret-Key": magic_admin.api_secret_key, + "User-Agent": json.dumps(user_agent), } def request(self, method, url, params=None, data=None): @@ -118,22 +117,24 @@ def _parse_and_convert_to_api_response(self, resp, request_params, request_data) resp_data = resp.json() raise error_class( - http_status=resp_data.get('status'), + http_status=resp_data.get("status"), http_code=status_code, - http_resp_data=resp_data.get('data'), - http_message=resp_data.get('message'), - http_error_code=resp_data.get('error_code'), + http_resp_data=resp_data.get("data"), + http_message=resp_data.get("message"), + http_error_code=resp_data.get("error_code"), http_request_params=request_params, http_request_data=request_data, http_method=resp.request.method, ) def _handle_request_error(self, e): - message = 'Unexpected error thrown while communicating to Magic. ' \ - 'Please reach out to support@magic.link if the problem continues. ' \ - 'Error message: {error_class} was raised - {error_message}'.format( + message = ( + "Unexpected error thrown while communicating to Magic. " + "Please reach out to support@magic.link if the problem continues. " + "Error message: {error_class} was raised - {error_message}".format( error_class=e.__class__.__name__, - error_message=str(e) or 'no error message.', + error_message=str(e) or "no error message.", ) + ) raise APIConnectionError(message) diff --git a/magic_admin/magic.py b/magic_admin/magic.py index 78a494d..21fdb88 100644 --- a/magic_admin/magic.py +++ b/magic_admin/magic.py @@ -14,8 +14,7 @@ class Magic: - - v1_client_info = base_url + '/v1/admin/client/get' + v1_client_info = base_url + "/v1/admin/client" def __getattr__(self, attribute_name): try: @@ -38,12 +37,16 @@ def __init__( self._resource.setup_request_client(retries, timeout, backoff_factor) self._set_api_secret_key(api_secret_key) init_requests_client = RequestsClient(retries, timeout, backoff_factor) - magic_admin.client_id = client_id or \ - init_requests_client.request('get', self.v1_client_info).data['client_id'] + magic_admin.client_id = ( + client_id + or init_requests_client.request("get", self.v1_client_info).data[ + "client_id" + ] + ) def _set_api_secret_key(self, api_secret_key): magic_admin.api_secret_key = api_secret_key or os.environ.get( - 'MAGIC_API_SECRET_KEY', + "MAGIC_API_SECRET_KEY", ) if magic_admin.api_secret_key is None: diff --git a/magic_admin/resources/__init__.py b/magic_admin/resources/__init__.py index a72526e..796264a 100644 --- a/magic_admin/resources/__init__.py +++ b/magic_admin/resources/__init__.py @@ -1,3 +1,3 @@ -from magic_admin.resources.token import Token -from magic_admin.resources.user import User -from magic_admin.resources.wallet import WalletType +from magic_admin.resources.token import Token # noqa: F401 +from magic_admin.resources.user import User # noqa: F401 +from magic_admin.resources.wallet import WalletType # noqa: F401 diff --git a/magic_admin/resources/base.py b/magic_admin/resources/base.py index 455b92d..ba0050f 100644 --- a/magic_admin/resources/base.py +++ b/magic_admin/resources/base.py @@ -3,9 +3,8 @@ class ResourceMeta(type): - def __init__(cls, name, bases, cls_dict): - if not hasattr(cls, '_registry'): + if not hasattr(cls, "_registry"): cls._registry = {} else: cls._registry[name] = cls() @@ -14,7 +13,6 @@ def __init__(cls, name, bases, cls_dict): class ResourceComponent(metaclass=ResourceMeta): - _base_url = base_url def __getattr__(self, resource_name): @@ -22,7 +20,7 @@ def __getattr__(self, resource_name): return self._registry[resource_name] else: raise AttributeError( - '{object_name} has no attribute \'{resource_name}\''.format( + "{object_name} has no attribute '{resource_name}'".format( object_name=self.__class__.__name__, resource_name=resource_name, ), @@ -32,10 +30,10 @@ def setup_request_client(self, retries, timeout, backoff_factor): _request_client = RequestsClient(retries, timeout, backoff_factor) for resource in self._registry.values(): - setattr(resource, '_request_client', _request_client) + setattr(resource, "_request_client", _request_client) def _construct_url(self, url_path): - return '{base_url}{url_path}'.format( + return "{base_url}{url_path}".format( base_url=self._base_url, url_path=url_path, ) diff --git a/magic_admin/resources/token.py b/magic_admin/resources/token.py index fdbc9b8..c0322e5 100644 --- a/magic_admin/resources/token.py +++ b/magic_admin/resources/token.py @@ -18,16 +18,17 @@ class Token(ResourceComponent): - - required_fields = frozenset([ - 'iat', - 'ext', - 'nbf', - 'iss', - 'sub', - 'aud', - 'tid', - ]) + required_fields = frozenset( + [ + "iat", + "ext", + "nbf", + "iss", + "sub", + "aud", + "tid", + ] + ) @classmethod def _check_required_fields(cls, claim): @@ -46,7 +47,7 @@ def _check_required_fields(cls, claim): if missing_fields: raise DIDTokenMalformed( - message='DID token is missing required field(s): {}'.format( + message="DID token is missing required field(s): {}".format( sorted(missing_fields), ), ) @@ -66,21 +67,21 @@ def decode(cls, did_token): """ try: decoded_did_token = json.loads( - base64.urlsafe_b64decode(did_token).decode('utf-8'), + base64.urlsafe_b64decode(did_token).decode("utf-8"), ) except Exception as e: raise DIDTokenMalformed( - message='DID token is malformed. It has to be a based64 encoded ' - 'JSON serialized string. {err} ({msg}).'.format( + message="DID token is malformed. It has to be a based64 encoded " + "JSON serialized string. {err} ({msg}).".format( err=e.__class__.__name__, - msg=str(e) or '', + msg=str(e) or "", ), ) if len(decoded_did_token) != EXPECTED_DID_TOKEN_CONTENT_LENGTH: raise DIDTokenMalformed( - message='DID token is malformed. It has to have two parts ' - '[proof, claim].', + message="DID token is malformed. It has to have two parts " + "[proof, claim].", ) proof = decoded_did_token[0] @@ -89,10 +90,10 @@ def decode(cls, did_token): claim = json.loads(decoded_did_token[1]) except Exception as e: raise DIDTokenMalformed( - message='DID token is malformed. Given claim should be a JSON ' - 'serialized string. {err} ({msg}).'.format( + message="DID token is malformed. Given claim should be a JSON " + "serialized string. {err} ({msg}).".format( err=e.__class__.__name__, - msg=str(e) or '', + msg=str(e) or "", ), ) @@ -113,7 +114,7 @@ def get_issuer(cls, did_token): """ _, claim = cls.decode(did_token) - return claim['iss'] + return claim["iss"] @classmethod def get_public_address(cls, did_token): @@ -141,15 +142,15 @@ def validate(cls, did_token): """ proof, claim = cls.decode(did_token) - if claim['ext'] is None: + if claim["ext"] is None: raise DIDTokenInvalid( message='Please check the "ext" field and regenerate a new token ' - 'with a suitable value.', + "with a suitable value.", ) recovered_address = w3.eth.account.recover_message( encode_defunct( - text=json.dumps(claim, separators=(',', ':')), + text=json.dumps(claim, separators=(",", ":")), ), signature=proof, ) @@ -157,24 +158,24 @@ def validate(cls, did_token): if recovered_address != cls.get_public_address(did_token): raise DIDTokenInvalid( message='Signature mismatch between "proof" and "claim". Please ' - 'generate a new token with an intended issuer.', + "generate a new token with an intended issuer.", ) current_time_in_s = epoch_time_now() - if current_time_in_s > claim['ext']: + if current_time_in_s > claim["ext"]: raise DIDTokenExpired( - message='Given DID token has expired. Please generate a new one.', + message="Given DID token has expired. Please generate a new one.", ) - if current_time_in_s < apply_did_token_nbf_grace_period(claim['nbf']): + if current_time_in_s < apply_did_token_nbf_grace_period(claim["nbf"]): raise DIDTokenInvalid( - message='Given DID token cannot be used at this time. Please ' + message="Given DID token cannot be used at this time. Please " 'check the "nbf" field and regenerate a new token with a suitable ' - 'value.', + "value.", ) - if claim['aud'] != magic_admin.client_id: + if claim["aud"] != magic_admin.client_id: raise DIDTokenInvalid( message='"aud" field does not match your client. Please check your secret key.', ) diff --git a/magic_admin/resources/user.py b/magic_admin/resources/user.py index 55af773..f0c60cb 100644 --- a/magic_admin/resources/user.py +++ b/magic_admin/resources/user.py @@ -4,13 +4,14 @@ class User(ResourceComponent): - - v1_user_info = '/v1/admin/auth/user/get' - v2_user_logout = '/v2/admin/auth/user/logout' + v1_user_info = "/v1/admin/user" + v1_user_logout = "/v1/admin/user/logout" def get_metadata_by_issuer_and_wallet(self, issuer, wallet_type): return self.request( - 'get', self.v1_user_info, params={'issuer': issuer, 'wallet_type': wallet_type}, + "get", + self.v1_user_info, + params={"issuer": issuer, "wallet_type": wallet_type}, ) def get_metadata_by_public_address_and_wallet(self, public_address, wallet_type): @@ -20,7 +21,9 @@ def get_metadata_by_public_address_and_wallet(self, public_address, wallet_type) ) def get_metadata_by_token_and_wallet(self, did_token, wallet_type): - return self.get_metadata_by_issuer_and_wallet(self.Token.get_issuer(did_token), wallet_type) + return self.get_metadata_by_issuer_and_wallet( + self.Token.get_issuer(did_token), wallet_type + ) def get_metadata_by_issuer(self, issuer): return self.get_metadata_by_issuer_and_wallet(issuer, WalletType.NONE) @@ -34,7 +37,7 @@ def get_metadata_by_token(self, did_token): return self.get_metadata_by_issuer(self.Token.get_issuer(did_token)) def logout_by_issuer(self, issuer): - return self.request('post', self.v2_user_logout, data={'issuer': issuer}) + return self.request("post", self.v1_user_logout, data={"issuer": issuer}) def logout_by_public_address(self, public_address): return self.logout_by_issuer( diff --git a/magic_admin/resources/wallet.py b/magic_admin/resources/wallet.py index b37952b..ca74cc9 100644 --- a/magic_admin/resources/wallet.py +++ b/magic_admin/resources/wallet.py @@ -2,25 +2,25 @@ class WalletType(Enum): - ETH = 'ETH' - HARMONY = 'HARMONY' - ICON = 'ICON' - FLOW = 'FLOW' - TEZOS = 'TEZOS' - ZILLIQA = 'ZILLIQA' - POLKADOT = 'POLKADOT' - SOLANA = 'SOLANA' - AVAX = 'AVAX' - ALGOD = 'ALGOD' - COSMOS = 'COSMOS' - CELO = 'CELO' - BITCOIN = 'BITCOIN' - NEAR = 'NEAR' - HELIUM = 'HELIUM' - CONFLUX = 'CONFLUX' - TERRA = 'TERRA' - TAQUITO = 'TAQUITO' - ED = 'ED' - HEDERA = 'HEDERA' - NONE = 'NONE' - ANY = 'ANY' + ETH = "ETH" + HARMONY = "HARMONY" + ICON = "ICON" + FLOW = "FLOW" + TEZOS = "TEZOS" + ZILLIQA = "ZILLIQA" + POLKADOT = "POLKADOT" + SOLANA = "SOLANA" + AVAX = "AVAX" + ALGOD = "ALGOD" + COSMOS = "COSMOS" + CELO = "CELO" + BITCOIN = "BITCOIN" + NEAR = "NEAR" + HELIUM = "HELIUM" + CONFLUX = "CONFLUX" + TERRA = "TERRA" + TAQUITO = "TAQUITO" + ED = "ED" + HEDERA = "HEDERA" + NONE = "NONE" + ANY = "ANY" diff --git a/magic_admin/response.py b/magic_admin/response.py index c16ff64..c2c67db 100644 --- a/magic_admin/response.py +++ b/magic_admin/response.py @@ -1,6 +1,5 @@ class MagicResponse: - def __init__(self, content, resp_data, status_code): self.content = content self.status_code = status_code - self.data = resp_data['data'] + self.data = resp_data diff --git a/magic_admin/utils/did_token.py b/magic_admin/utils/did_token.py index bd8ede6..2df3ade 100644 --- a/magic_admin/utils/did_token.py +++ b/magic_admin/utils/did_token.py @@ -12,13 +12,13 @@ def parse_public_address_from_issuer(issuer): public_address (str): An Ethereum public key. """ try: - return issuer.split(':')[2] + return issuer.split(":")[2] except IndexError: raise DIDTokenMalformed( - 'Given issuer ({}) is malformed. Please make sure it follows the ' - '`did:method-name:method-specific-id` format.'.format(issuer), + "Given issuer ({}) is malformed. Please make sure it follows the " + "`did:method-name:method-specific-id` format.".format(issuer), ) def construct_issuer_with_public_address(public_address): - return 'did:ethr:{}'.format(public_address) + return "did:ethr:{}".format(public_address) diff --git a/magic_admin/utils/http.py b/magic_admin/utils/http.py index 58eea59..8d0ffd6 100644 --- a/magic_admin/utils/http.py +++ b/magic_admin/utils/http.py @@ -1,10 +1,10 @@ import re -AUTHORIZATION_PATTERN = r'Bearer (?P.+)' +AUTHORIZATION_PATTERN = r"Bearer (?P.+)" def null_safe(value): - if value is None or value in ['null', 'none', 'None', '']: + if value is None or value in ["null", "none", "None", ""]: return None return value @@ -16,4 +16,4 @@ def parse_authorization_header_value(header_value): if m is None: return None - return null_safe(m.group('token')) + return null_safe(m.group("token")) diff --git a/magic_admin/version.py b/magic_admin/version.py index ee65984..2101409 100644 --- a/magic_admin/version.py +++ b/magic_admin/version.py @@ -1 +1 @@ -VERSION = '1.2.0' +VERSION = "2.0.0" diff --git a/requirements-dev.txt b/requirements-dev.txt index 8c74560..cf7c655 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,38 +1,38 @@ -appnope==0.1.0 +appnope==0.1.4 aspy.yaml==1.3.0 -attrs==22.2.0 -backcall==0.1.0 -cfgv==3.1.0 -coverage==5.2.1 -decorator==4.4.2 -identify==1.4.14 -importlib-metadata==1.6.0 -importlib-resources==1.4.0 -ipdb==0.12.3 -ipython==7.11.1 +attrs==25.3.0 +backcall==0.2.0 +cfgv==3.4.0 +coverage==7.9.2 +decorator==5.2.1 +identify==2.6.12 +importlib-metadata==8.7.0 +importlib-resources==6.5.2 +ipdb==0.13.13 +ipython==9.4.0 ipython-genutils==0.2.0 -jedi==0.16.0 -more-itertools==8.2.0 -nodeenv==1.3.5 -packaging==23.2 -parso==0.6.2 -pexpect==4.8.0 +jedi==0.19.2 +more-itertools==10.7.0 +nodeenv==1.9.1 +packaging==25.0 +parso==0.8.4 +pexpect==4.9.0 pickleshare==0.7.5 -pluggy==1.4 -pre-commit==3.7.0 -pretend==1.0.8 -prompt-toolkit==3.0.5 -ptyprocess==0.6.0 +pluggy==1.6.0 +pre-commit==4.2.0 +pretend==1.0.9 +prompt-toolkit==3.0.51 +ptyprocess==0.7.0 py==1.11.0 -Pygments==2.7.4 -pyparsing==2.4.7 -pytest== 8.1.1 -pytest-cov==5.0.0 -pytest-mock==3.14.0 -six==1.14.0 -toml==0.10.0 -tox==4.14.0 -traitlets==4.3.3 -virtualenv==20.25 -wcwidth==0.1.9 -zipp==3.1.0 +Pygments==2.19.2 +pyparsing==3.2.3 +pytest==8.4.1 +pytest-cov==6.2.1 +pytest-mock==3.14.1 +six==1.17.0 +toml==0.10.2 +tox==4.28.1 +traitlets==5.14.3 +virtualenv==20.32.0 +wcwidth==0.2.13 +zipp==3.23.0 diff --git a/requirements.txt b/requirements.txt index 12db4ca..455c454 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -requests >= 2.22.0, <3 -web3 >= 6 -websockets >= 10.0 +requests==2.32.4 +web3==7.12.1 +websockets==15.0.1 diff --git a/setup.cfg b/setup.cfg index 2e9053c..d28d0fa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,4 @@ universal = 1 [metadata] license_file = LICENSE.txt +python_requires = >=3.11 diff --git a/setup.py b/setup.py index 38da7ae..a9e0fef 100644 --- a/setup.py +++ b/setup.py @@ -4,65 +4,65 @@ from setuptools import find_packages from setuptools import setup -with open('README.md') as fh: +with open("README.md") as fh: long_description = fh.read() def read_version(): version_contents = {} - with open(join(dirname(__file__), 'magic_admin', 'version.py')) as fh: + with open(join(dirname(__file__), "magic_admin", "version.py")) as fh: exec(fh.read(), version_contents) - return version_contents['VERSION'] + return version_contents["VERSION"] def load_readme(): - with open(join(dirname(__file__), 'README.md')) as fh: + with open(join(dirname(__file__), "README.md")) as fh: long_description = fh.read() return long_description def load_requirements(): - with open(join(dirname(__file__), 'requirements.txt')) as fh: + with open(join(dirname(__file__), "requirements.txt")) as fh: requirements = fh.readlines() return requirements setup( - name='magic-admin', + name="magic-admin", version=read_version(), - description='Magic Python Library', + description="Magic Python Library", long_description=load_readme(), - long_description_content_type='text/markdown', - author='Magic', - author_email='support@magic.link', - url='https://magic.link', - license='MIT', - keywords='magic python sdk', + long_description_content_type="text/markdown", + author="Magic", + author_email="support@magic.link", + url="https://magic.link", + license="MIT", + keywords="magic python sdk", packages=find_packages( exclude=[ - 'tests', - 'tests.*', - 'testing', - 'testing.*', - 'virtualenv_run', - 'virtualenv_run.*', + "tests", + "tests.*", + "testing", + "testing.*", + "virtualenv_run", + "virtualenv_run.*", ], ), zip_safe=False, install_requires=load_requirements(), - python_requires='>=3.11', + python_requires=">=3.11", project_urls={ - 'Website': 'https://magic.link', + "Website": "https://magic.link", }, classifiers=[ - 'Development Status :: 3 - Alpha', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.11', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', + "Development Status :: 3 - Alpha", + "Programming Language :: Python", + "Programming Language :: Python :: 3.11", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", ], ) diff --git a/testing/data/did_token.py b/testing/data/did_token.py index dfd474a..c07ed0d 100644 --- a/testing/data/did_token.py +++ b/testing/data/did_token.py @@ -1,30 +1,34 @@ -public_address = '0x4B73C58370AEfcEf86A6021afCDe5673511376B2' +public_address = "0x4B73C58370AEfcEf86A6021afCDe5673511376B2" -issuer = 'did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2' +issuer = "did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2" -proof = '0xaa50be70729ca705ba7c8d00185c6f2da479d0fcde5311ca4ce5b1ba715c8a721c5' \ - 'f1948434f96ff577d7b2b6ad82d3dd5a2457fe6998b137ed9bc08d36e549c1b' +proof = ( + "0xaa50be70729ca705ba7c8d00185c6f2da479d0fcde5311ca4ce5b1ba715c8a721c5" + "f1948434f96ff577d7b2b6ad82d3dd5a2457fe6998b137ed9bc08d36e549c1b" +) claim = { - 'iat': 1586764270, - 'ext': 11173528500, - 'iss': 'did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2', - 'sub': 'NjrA53ScQ8IV80NJnx4t3Shi9-kFfF5qavD2Vr0d1dc=', - 'aud': 'did:magic:731848cc-084e-41ff-bbdf-7f103817ea6b', - 'nbf': 1586764270, - 'tid': 'ebcc880a-ffc9-4375-84ae-154ccd5c746d', - 'add': '0x84d6839268a1af9111fdeccd396f303805dca2bc03450b7eb116e2f5fc8c5a722' - 'd1fb9af233aa73c5c170839ce5ad8141b9b4643380982da4bfbb0b11284988f1b', + "iat": 1586764270, + "ext": 11173528500, + "iss": "did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2", + "sub": "NjrA53ScQ8IV80NJnx4t3Shi9-kFfF5qavD2Vr0d1dc=", + "aud": "did:magic:731848cc-084e-41ff-bbdf-7f103817ea6b", + "nbf": 1586764270, + "tid": "ebcc880a-ffc9-4375-84ae-154ccd5c746d", + "add": "0x84d6839268a1af9111fdeccd396f303805dca2bc03450b7eb116e2f5fc8c5a722" + "d1fb9af233aa73c5c170839ce5ad8141b9b4643380982da4bfbb0b11284988f1b", } -future_did_token = 'WyIweGFhNTBiZTcwNzI5Y2E3MDViYTdjOGQwMDE4NWM2ZjJkYTQ3OWQwZm' \ - 'NkZTUzMTFjYTRjZTViMWJhNzE1YzhhNzIxYzVmMTk0ODQzNGY5NmZmNTc3ZDdiMmI2YWQ4MmQ' \ - 'zZGQ1YTI0NTdmZTY5OThiMTM3ZWQ5YmMwOGQzNmU1NDljMWIiLCJ7XCJpYXRcIjoxNTg2NzY0' \ - 'MjcwLFwiZXh0XCI6MTExNzM1Mjg1MDAsXCJpc3NcIjpcImRpZDpldGhyOjB4NEI3M0M1ODM3M' \ - 'EFFZmNFZjg2QTYwMjFhZkNEZTU2NzM1MTEzNzZCMlwiLFwic3ViXCI6XCJOanJBNTNTY1E4SV' \ - 'Y4ME5Kbng0dDNTaGk5LWtGZkY1cWF2RDJWcjBkMWRjPVwiLFwiYXVkXCI6XCJkaWQ6bWFnaWM' \ - '6NzMxODQ4Y2MtMDg0ZS00MWZmLWJiZGYtN2YxMDM4MTdlYTZiXCIsXCJuYmZcIjoxNTg2NzY0' \ - 'MjcwLFwidGlkXCI6XCJlYmNjODgwYS1mZmM5LTQzNzUtODRhZS0xNTRjY2Q1Yzc0NmRcIixcI' \ - 'mFkZFwiOlwiMHg4NGQ2ODM5MjY4YTFhZjkxMTFmZGVjY2QzOTZmMzAzODA1ZGNhMmJjMDM0NT' \ - 'BiN2ViMTE2ZTJmNWZjOGM1YTcyMmQxZmI5YWYyMzNhYTczYzVjMTcwODM5Y2U1YWQ4MTQxYjl' \ - 'iNDY0MzM4MDk4MmRhNGJmYmIwYjExMjg0OTg4ZjFiXCJ9Il0=' +future_did_token = ( + "WyIweGFhNTBiZTcwNzI5Y2E3MDViYTdjOGQwMDE4NWM2ZjJkYTQ3OWQwZm" + "NkZTUzMTFjYTRjZTViMWJhNzE1YzhhNzIxYzVmMTk0ODQzNGY5NmZmNTc3ZDdiMmI2YWQ4MmQ" + "zZGQ1YTI0NTdmZTY5OThiMTM3ZWQ5YmMwOGQzNmU1NDljMWIiLCJ7XCJpYXRcIjoxNTg2NzY0" + "MjcwLFwiZXh0XCI6MTExNzM1Mjg1MDAsXCJpc3NcIjpcImRpZDpldGhyOjB4NEI3M0M1ODM3M" + "EFFZmNFZjg2QTYwMjFhZkNEZTU2NzM1MTEzNzZCMlwiLFwic3ViXCI6XCJOanJBNTNTY1E4SV" + "Y4ME5Kbng0dDNTaGk5LWtGZkY1cWF2RDJWcjBkMWRjPVwiLFwiYXVkXCI6XCJkaWQ6bWFnaWM" + "6NzMxODQ4Y2MtMDg0ZS00MWZmLWJiZGYtN2YxMDM4MTdlYTZiXCIsXCJuYmZcIjoxNTg2NzY0" + "MjcwLFwidGlkXCI6XCJlYmNjODgwYS1mZmM5LTQzNzUtODRhZS0xNTRjY2Q1Yzc0NmRcIixcI" + "mFkZFwiOlwiMHg4NGQ2ODM5MjY4YTFhZjkxMTFmZGVjY2QzOTZmMzAzODA1ZGNhMmJjMDM0NT" + "BiN2ViMTE2ZTJmNWZjOGM1YTcyMmQxZmI5YWYyMzNhYTczYzVjMTcwODM5Y2U1YWQ4MTQxYjl" + "iNDY0MzM4MDk4MmRhNGJmYmIwYjExMjg0OTg4ZjFiXCJ9Il0=" +) diff --git a/tests/integration/magic_test.py b/tests/integration/magic_test.py index 2a03160..38c5802 100644 --- a/tests/integration/magic_test.py +++ b/tests/integration/magic_test.py @@ -9,8 +9,7 @@ class TestMagic: - - api_secret_key = 'troll_goat' + api_secret_key = "troll_goat" @pytest.fixture(autouse=True) def setup(self): @@ -18,13 +17,15 @@ def setup(self): request=mock.Mock( return_value=mock.Mock( data={ - 'client_id': '1234', + "client_id": "1234", }, ), ), ) # self.mocked_rc.request= - with mock.patch('magic_admin.magic.RequestsClient', return_value=self.mocked_rc): + with mock.patch( + "magic_admin.magic.RequestsClient", return_value=self.mocked_rc + ): yield def test_init_with_secret_key(self): @@ -33,7 +34,7 @@ def test_init_with_secret_key(self): assert magic_admin.api_secret_key == self.api_secret_key @pytest.mark.parametrize( - 'resource_name', + "resource_name", ResourceComponent._registry.keys(), ) def test_init_with_request_client_set_on_resources(self, resource_name): @@ -42,7 +43,7 @@ def test_init_with_request_client_set_on_resources(self, resource_name): assert getattr(magic, resource_name)._request_client @pytest.mark.parametrize( - 'resource_name', + "resource_name", ResourceComponent._registry.keys(), ) def test_gets_resource(self, resource_name): diff --git a/tests/integration/resources/token_test.py b/tests/integration/resources/token_test.py index b52ff9d..83e5539 100644 --- a/tests/integration/resources/token_test.py +++ b/tests/integration/resources/token_test.py @@ -11,7 +11,6 @@ class TestToken: - def test_check_required_fields(self): Token._check_required_fields(claim) @@ -26,7 +25,7 @@ def test_get_public_address(self): def test_validate(self): with mock.patch( - 'magic_admin.resources.token.magic_admin', - new=stub(client_id='did:magic:731848cc-084e-41ff-bbdf-7f103817ea6b'), + "magic_admin.resources.token.magic_admin", + new=stub(client_id="did:magic:731848cc-084e-41ff-bbdf-7f103817ea6b"), ): Token.validate(future_did_token) diff --git a/tests/unit/config_test.py b/tests/unit/config_test.py index 6c93d6e..a123fee 100644 --- a/tests/unit/config_test.py +++ b/tests/unit/config_test.py @@ -2,4 +2,4 @@ def test_base_url(): - assert base_url == 'https://api.magic.link' + assert base_url == "https://api.toaster.magic.link" diff --git a/tests/unit/error_test.py b/tests/unit/error_test.py index 6e4f82f..8372406 100644 --- a/tests/unit/error_test.py +++ b/tests/unit/error_test.py @@ -10,59 +10,57 @@ class MagicErrorBase: - error_class = None - message = 'Magic is amazing' + message = "Magic is amazing" def test_str(self): assert str(self.error_class(self.message)) == self.message def test_str_with_empty_message(self): - assert str(self.error_class()) == '' + assert str(self.error_class()) == "" def test_repr(self): - assert repr(self.error_class(self.message)) == '{}(message=\'Magic is ' \ - 'amazing\')'.format(self.error_class.__name__) + assert repr( + self.error_class(self.message) + ) == "{}(message='Magic is amazing')".format(self.error_class.__name__) def test_to_dict(self): - assert self.error_class(self.message).to_dict() == {'message': str(self.message)} + assert self.error_class(self.message).to_dict() == { + "message": str(self.message) + } class TestMagicError(MagicErrorBase): - error_class = MagicError class TestDIDTokenInvalid(MagicErrorBase): - error_class = DIDTokenInvalid class TestAPIConnectionError(MagicErrorBase): - error_class = APIConnectionError class RequestErrorBase: - error_class = None - message = 'Magic is amazing' - http_status = 'success' + message = "Magic is amazing" + http_status = "success" http_code = 200 - http_resp_data = {'magic': 'link'} - http_message = 'Troll goat is cute' - http_error_code = 'TROLL_GOAT_IS_CUTE' - http_request_params = 'a=b&b=c' - http_request_data = {'magic': 'link'} - http_method = 'post' + http_resp_data = {"magic": "link"} + http_message = "Troll goat is cute" + http_error_code = "TROLL_GOAT_IS_CUTE" + http_request_params = "a=b&b=c" + http_request_data = {"magic": "link"} + http_method = "post" def test_str(self): assert str(self.error_class(self.message)) == self.message def test_str_with_empty_message(self): - assert str(self.error_class()) == '' + assert str(self.error_class()) == "" def test_repr(self): assert repr( @@ -71,7 +69,7 @@ def test_repr(self): http_error_code=self.http_error_code, http_code=self.http_code, ), - ) == '{}(message=\'Magic is amazing\', http_error_code={}, http_code={}).'.format( + ) == "{}(message='Magic is amazing', http_error_code={}, http_code={}).".format( self.error_class.__name__, self.http_error_code, self.http_code, @@ -89,43 +87,37 @@ def test_to_dict(self): http_request_data=self.http_request_data, http_method=self.http_method, ).to_dict() == { - 'message': self.message, - 'http_error_code': self.http_error_code, - 'http_code': self.http_code, - 'http_status': self.http_status, - 'http_resp_data': self.http_resp_data, - 'http_message': self.http_message, - 'http_request_params': self.http_request_params, - 'http_request_data': self.http_request_data, - 'http_method': self.http_method, + "message": self.message, + "http_error_code": self.http_error_code, + "http_code": self.http_code, + "http_status": self.http_status, + "http_resp_data": self.http_resp_data, + "http_message": self.http_message, + "http_request_params": self.http_request_params, + "http_request_data": self.http_request_data, + "http_method": self.http_method, } class TestRequestError(RequestErrorBase): - error_class = RequestError class TestRateLimitingError(RequestErrorBase): - error_class = RateLimitingError class TestBadRequestError(RequestErrorBase): - error_class = BadRequestError class TestAuthenticationError(RequestErrorBase): - error_class = AuthenticationError class TestForbiddenError(RequestErrorBase): - error_class = ForbiddenError class TestAPIError(RequestErrorBase): - error_class = APIError diff --git a/tests/unit/http_client_test.py b/tests/unit/http_client_test.py index db801e6..f048c56 100644 --- a/tests/unit/http_client_test.py +++ b/tests/unit/http_client_test.py @@ -18,14 +18,13 @@ class TestRequestsClient: - retries = 1 timeout = 2 backoff_factor = 3 def test_init(self): with mock.patch( - 'magic_admin.http_client.RequestsClient._setup_request_session', + "magic_admin.http_client.RequestsClient._setup_request_session", ) as mock_setup_request_session: rc = RequestsClient(self.retries, self.timeout, self.backoff_factor) @@ -36,9 +35,9 @@ def test_init(self): mock_setup_request_session.assert_called_once_with() def test_get_platform_info(self): - platform_name = 'troll_goat' - py_version = '9.0.0.0' - error_msg = 'error_msg' + platform_name = "troll_goat" + py_version = "9.0.0.0" + error_msg = "error_msg" platform = mock.Mock( platform=mock.Mock(return_value=platform_name), @@ -47,13 +46,13 @@ def test_get_platform_info(self): ) with mock.patch( - 'magic_admin.http_client.platform', + "magic_admin.http_client.platform", platform, ): assert RequestsClient._get_platform_info() == { - 'platform': platform_name, - 'language_version': py_version, - 'uname': '<{}>'.format(error_msg), + "platform": platform_name, + "language_version": py_version, + "uname": "<{}>".format(error_msg), } platform.platform.assert_called_once_with() @@ -61,13 +60,17 @@ def test_get_platform_info(self): platform.uname.assert_called_once_with() def test_setup_request_session(self): - with mock.patch( - 'magic_admin.http_client.Session', - ) as mock_session, mock.patch( - 'magic_admin.http_client.HTTPAdapter', - ) as mock_http_adapter, mock.patch( - 'magic_admin.http_client.Retry', - ) as mock_retry: + with ( + mock.patch( + "magic_admin.http_client.Session", + ) as mock_session, + mock.patch( + "magic_admin.http_client.HTTPAdapter", + ) as mock_http_adapter, + mock.patch( + "magic_admin.http_client.Retry", + ) as mock_retry, + ): RequestsClient(self.retries, self.timeout, self.backoff_factor) mock_retry.assert_called_once_with( @@ -85,23 +88,25 @@ def test_setup_request_session(self): def test_get_request_headers(self): rc = RequestsClient(self.retries, self.timeout, self.backoff_factor) - platform_info = {'troll': 'goat'} - magic_admin.api_secret_key = 'magic_secret_key' + platform_info = {"troll": "goat"} + magic_admin.api_secret_key = "magic_secret_key" with mock.patch.object( rc, - '_get_platform_info', + "_get_platform_info", return_value=platform_info, ) as mock_get_platform_info: assert rc._get_request_headers() == { - 'X-Magic-Secret-Key': magic_admin.api_secret_key, - 'User-Agent': json.dumps({ - 'language': 'python', - 'sdk_version': version.VERSION, - 'publisher': 'magic', - 'http_lib': rc.__class__.__name__, - **platform_info, - }), + "X-Magic-Secret-Key": magic_admin.api_secret_key, + "User-Agent": json.dumps( + { + "language": "python", + "sdk_version": version.VERSION, + "publisher": "magic", + "http_lib": rc.__class__.__name__, + **platform_info, + } + ), } mock_get_platform_info.assert_called_once_with() @@ -115,60 +120,63 @@ def test_get_request_headers_raises_error(self): def test_handle_request_error(self): rc = RequestsClient(self.retries, self.timeout, self.backoff_factor) - exception = Exception('troll_goat') + exception = Exception("troll_goat") with pytest.raises(APIConnectionError) as e: rc._handle_request_error(exception) assert str(e.value) == ( - 'Unexpected error thrown while communicating to Magic. ' - 'Please reach out to support@magic.link if the problem continues. ' - 'Error message: {error_class} was raised - {error_message}'.format( + "Unexpected error thrown while communicating to Magic. " + "Please reach out to support@magic.link if the problem continues. " + "Error message: {error_class} was raised - {error_message}".format( error_class=exception.__class__.__name__, - error_message=str(exception) or 'no error message.', + error_message=str(exception) or "no error message.", ) ) class TestRequestClientRequest: - retries = 1 timeout = 2 backoff_factor = 3 mock_tuple = namedtuple( - 'mock_tuple', + "mock_tuple", [ - 'get_request_headers', - 'handle_request_error', - 'parse_and_convert_to_api_response', + "get_request_headers", + "handle_request_error", + "parse_and_convert_to_api_response", ], ) @pytest.fixture(autouse=True) def setup(self): - self.some_headers = {'troll': 'goat'} - self.method = 'post' - self.url = '/path' - self.params = 'params' - self.data = 'data' + self.some_headers = {"troll": "goat"} + self.method = "post" + self.url = "/path" + self.params = "params" + self.data = "data" self.rc = RequestsClient(self.retries, self.timeout, self.backoff_factor) self.rc.http = mock.Mock() @pytest.fixture def mock_funcs(self): - with mock.patch.object( - self.rc, - '_get_request_headers', - return_value=self.some_headers, - ) as mock_get_request_headers, mock.patch.object( - self.rc, - '_handle_request_error', - ) as mock_handle_request_error, mock.patch.object( - self.rc, - '_parse_and_convert_to_api_response', - ) as mock_parse_and_convert_to_api_response: + with ( + mock.patch.object( + self.rc, + "_get_request_headers", + return_value=self.some_headers, + ) as mock_get_request_headers, + mock.patch.object( + self.rc, + "_handle_request_error", + ) as mock_handle_request_error, + mock.patch.object( + self.rc, + "_parse_and_convert_to_api_response", + ) as mock_parse_and_convert_to_api_response, + ): yield self.mock_tuple( mock_get_request_headers, mock_handle_request_error, @@ -176,12 +184,15 @@ def mock_funcs(self): ) def test_request_no_exception_and_returns_api_response(self, mock_funcs): - assert self.rc.request( - self.method, - self.url, - params=self.params, - data=self.data, - ) == mock_funcs.parse_and_convert_to_api_response.return_value + assert ( + self.rc.request( + self.method, + self.url, + params=self.params, + data=self.data, + ) + == mock_funcs.parse_and_convert_to_api_response.return_value + ) mock_funcs.get_request_headers.assert_called_once_with() self.rc.http.request.assert_called_once_with( @@ -203,12 +214,15 @@ def test_request_exceptions_and_handles_error(self, mock_funcs): exception = Exception() self.rc.http.request = mock.Mock(side_effect=exception) - assert self.rc.request( - self.method, - self.url, - params=self.params, - data=self.data, - ) == mock_funcs.handle_request_error.return_value + assert ( + self.rc.request( + self.method, + self.url, + params=self.params, + data=self.data, + ) + == mock_funcs.handle_request_error.return_value + ) mock_funcs.get_request_headers.assert_called_once_with() self.rc.http.request.assert_called_once_with( @@ -224,20 +238,19 @@ def test_request_exceptions_and_handles_error(self, mock_funcs): class TestParseAndConvertToAPIResponse: - retries = 1 timeout = 2 backoff_factor = 3 @pytest.fixture(autouse=True) def setup(self): - self.params = 'params' - self.request_data = 'request_data' + self.params = "params" + self.request_data = "request_data" self.data = { - 'data': 'troll_goat', - 'status': 'failed', - 'message': 'troll_goat_is_cute', - 'error_code': 'some_error', + "data": "troll_goat", + "status": "failed", + "message": "troll_goat_is_cute", + "error_code": "some_error", } self.rc = RequestsClient(self.retries, self.timeout, self.backoff_factor) @@ -255,10 +268,10 @@ def test_ok_response(self): assert isinstance(parsed_resp, MagicResponse) assert parsed_resp.content == self.resp.content assert parsed_resp.status_code == self.resp.status_code - assert parsed_resp.data == self.data['data'] + assert parsed_resp.data == self.data @pytest.mark.parametrize( - 'status_code,error_class', + "status_code,error_class", [ (400, BadRequestError), (401, AuthenticationError), @@ -279,13 +292,13 @@ def test_client_error_response(self, status_code, error_class): ) assert e.value.to_dict() == { - 'http_status': self.data['status'], - 'http_code': self.resp.status_code, - 'http_resp_data': self.data['data'], - 'http_message': self.data['message'], - 'http_error_code': self.data['error_code'], - 'http_request_params': self.params, - 'http_request_data': self.request_data, - 'http_method': self.resp.request.method, - 'message': mock.ANY, + "http_status": self.data["status"], + "http_code": self.resp.status_code, + "http_resp_data": self.data["data"], + "http_message": self.data["message"], + "http_error_code": self.data["error_code"], + "http_request_params": self.params, + "http_request_data": self.request_data, + "http_method": self.resp.request.method, + "message": mock.ANY, } diff --git a/tests/unit/magic_test.py b/tests/unit/magic_test.py index c64111e..fc0428e 100644 --- a/tests/unit/magic_test.py +++ b/tests/unit/magic_test.py @@ -11,8 +11,7 @@ class TestMagic: - - api_secret_key = 'troll_goat' + api_secret_key = "troll_goat" @pytest.fixture(autouse=True) def setup(self): @@ -21,17 +20,20 @@ def setup(self): request=mock.Mock( return_value=mock.Mock( data={ - 'client_id': '1234', + "client_id": "1234", }, ), ), ) - with mock.patch( - 'magic_admin.magic.ResourceComponent', - return_value=self.mocked_resource_component, - ), mock.patch( - 'magic_admin.magic.RequestsClient', - return_value=self.mocked_request_client, + with ( + mock.patch( + "magic_admin.magic.ResourceComponent", + return_value=self.mocked_resource_component, + ), + mock.patch( + "magic_admin.magic.RequestsClient", + return_value=self.mocked_request_client, + ), ): yield @@ -42,7 +44,7 @@ def teardown(self): def test_init(self): with mock.patch( - 'magic_admin.magic.Magic._set_api_secret_key', + "magic_admin.magic.Magic._set_api_secret_key", ) as mock_set_api_secret_key: Magic(api_secret_key=self.api_secret_key) @@ -57,13 +59,13 @@ def test_retrieves_secret_key_from_env_variable(self): assert magic_admin.api_secret_key is None with mock.patch( - 'os.environ.get', + "os.environ.get", return_value=self.api_secret_key, ) as mock_env_get: Magic() assert magic_admin.api_secret_key == self.api_secret_key - mock_env_get.assert_called_once_with('MAGIC_API_SECRET_KEY') + mock_env_get.assert_called_once_with("MAGIC_API_SECRET_KEY") def test_retrieves_secret_key_from_the_passed_in_value(self): assert magic_admin.api_secret_key is None diff --git a/tests/unit/resources/base_test.py b/tests/unit/resources/base_test.py index c7e6d19..784f6f5 100644 --- a/tests/unit/resources/base_test.py +++ b/tests/unit/resources/base_test.py @@ -6,15 +6,14 @@ class TestResourceComponent: - retries = 1 timeout = 2 backoff_factor = 3 - method = 'get' - url_path = '/troll/goat' - params = 'params' - data = 'data' + method = "get" + url_path = "/troll/goat" + params = "params" + data = "data" @pytest.fixture(autouse=True) def setup(self): @@ -22,7 +21,7 @@ def setup(self): def test_setup_request_client(self): with mock.patch( - 'magic_admin.resources.base.RequestsClient', + "magic_admin.resources.base.RequestsClient", ) as mock_request_client: self.rc.setup_request_client( self.retries, @@ -36,11 +35,12 @@ def test_setup_request_client(self): self.backoff_factor, ) for resource in self.rc._registry.values(): - assert getattr(resource, '_request_client') == \ - mock_request_client.return_value + assert ( + getattr(resource, "_request_client") == mock_request_client.return_value + ) def test_construct_url(self): - assert self.rc._construct_url(self.url_path) == '{}{}'.format( + assert self.rc._construct_url(self.url_path) == "{}{}".format( self.rc._base_url, self.url_path, ) @@ -50,13 +50,16 @@ def test_request(self): with mock.patch.object( self.rc, - '_construct_url', + "_construct_url", ) as mock_construct_url: - assert self.rc.request( - self.method, - self.url_path, - params=self.params, - data=self.data, - ) == self.rc._request_client.request.return_value + assert ( + self.rc.request( + self.method, + self.url_path, + params=self.params, + data=self.data, + ) + == self.rc._request_client.request.return_value + ) mock_construct_url.assert_called_once_with(self.url_path) diff --git a/tests/unit/resources/token_test.py b/tests/unit/resources/token_test.py index aba9f4f..6e44163 100644 --- a/tests/unit/resources/token_test.py +++ b/tests/unit/resources/token_test.py @@ -12,38 +12,39 @@ class TestToken: - - did_token = 'magic_token' - public_address = 'magic_address' - issuer = 'did:ethr:{}'.format(public_address) + did_token = "magic_token" + public_address = "magic_address" + issuer = "did:ethr:{}".format(public_address) @staticmethod def _generate_claim(fields=None): return {field: mock.ANY for field in fields or Token.required_fields} def test_required_fields(self): - assert Token.required_fields.difference( - {'nbf', 'sub', 'iss', 'ext', 'aud', 'tid', 'iat'}, - ) == frozenset() + assert ( + Token.required_fields.difference( + {"nbf", "sub", "iss", "ext", "aud", "tid", "iat"}, + ) + == frozenset() + ) def test_check_required_fields_raises_error(self): with pytest.raises(DIDTokenMalformed) as e: Token._check_required_fields( - self._generate_claim(fields=['nbf', 'sub', 'aud', 'tid', 'iat']), + self._generate_claim(fields=["nbf", "sub", "aud", "tid", "iat"]), ) - assert str(e.value) == 'DID token is missing required field(s): ' \ - '[\'ext\', \'iss\']' + assert str(e.value) == "DID token is missing required field(s): ['ext', 'iss']" def test_check_required_fields_passes(self): Token._check_required_fields(self._generate_claim()) def test_get_issuer_passes(self): - mocked_claim = {'iss': self.issuer} + mocked_claim = {"iss": self.issuer} with mock.patch.object( Token, - 'decode', + "decode", return_value=(mock.ANY, mocked_claim), ) as mock_decode: assert Token.get_issuer(self.did_token) == self.issuer @@ -51,13 +52,16 @@ def test_get_issuer_passes(self): mock_decode.assert_called_once_with(self.did_token) def test_get_public_address_passes(self): - with mock.patch( - 'magic_admin.resources.token.parse_public_address_from_issuer', - return_value=self.public_address, - ) as mock_parse_public_address, mock.patch.object( - Token, - 'get_issuer', - ) as mock_get_issuer: + with ( + mock.patch( + "magic_admin.resources.token.parse_public_address_from_issuer", + return_value=self.public_address, + ) as mock_parse_public_address, + mock.patch.object( + Token, + "get_issuer", + ) as mock_get_issuer, + ): assert Token.get_public_address(self.did_token) == self.public_address mock_get_issuer.assert_called_once_with(self.did_token) @@ -65,19 +69,21 @@ def test_get_public_address_passes(self): class TestTokenDecode: + did_token = "magic_token" + public_address = "magic_address" - did_token = 'magic_token' - public_address = 'magic_address' - - mock_funcs = namedtuple('mock_funcs', 'urlsafe_b64decode, json_loads') + mock_funcs = namedtuple("mock_funcs", "urlsafe_b64decode, json_loads") @pytest.fixture def setup_mocks(self): - with mock.patch( - 'magic_admin.resources.token.base64.urlsafe_b64decode', - ) as mock_urlsafe_b64decode, mock.patch( - 'magic_admin.resources.token.json.loads', - ) as mock_json_loads: + with ( + mock.patch( + "magic_admin.resources.token.base64.urlsafe_b64decode", + ) as mock_urlsafe_b64decode, + mock.patch( + "magic_admin.resources.token.json.loads", + ) as mock_json_loads, + ): yield self.mock_funcs(mock_urlsafe_b64decode, mock_json_loads) def test_decode_raises_error_if_did_token_is_malformed(self, setup_mocks): @@ -87,11 +93,13 @@ def test_decode_raises_error_if_did_token_is_malformed(self, setup_mocks): Token.decode(self.did_token) setup_mocks.urlsafe_b64decode.assert_called_once_with(self.did_token) - assert str(e.value) == 'DID token is malformed. It has to be a based64 ' \ - 'encoded JSON serialized string. Exception ().' + assert ( + str(e.value) == "DID token is malformed. It has to be a based64 " + "encoded JSON serialized string. Exception ()." + ) def test_decode_raises_error_if_did_token_has_missing_parts(self, setup_mocks): - setup_mocks.json_loads.return_value = ('miss one part') + setup_mocks.json_loads.return_value = "miss one part" with pytest.raises(DIDTokenMalformed) as e: Token.decode(self.did_token) @@ -100,13 +108,15 @@ def test_decode_raises_error_if_did_token_has_missing_parts(self, setup_mocks): setup_mocks.json_loads.assert_called_once_with( setup_mocks.urlsafe_b64decode.return_value.decode.return_value, ) - assert str(e.value) == 'DID token is malformed. It has to have two parts ' \ - '[proof, claim].' + assert ( + str(e.value) == "DID token is malformed. It has to have two parts " + "[proof, claim]." + ) def test_decode_raises_error_if_claim_is_not_json_serializable(self, setup_mocks): with pytest.raises(DIDTokenMalformed) as e: setup_mocks.json_loads.side_effect = [ - ('proof_in_str', 'claim_in_str'), # Succeeds the first time. + ("proof_in_str", "claim_in_str"), # Succeeds the first time. Exception(), # Fails the second time. ] @@ -115,81 +125,90 @@ def test_decode_raises_error_if_claim_is_not_json_serializable(self, setup_mocks setup_mocks.urlsafe_b64decode.assert_called_once_with(self.did_token) assert setup_mocks.json_loads.call_args_list == [ mock.call(setup_mocks.urlsafe_b64decode.return_value.decode.return_value), - mock.call('claim_in_str'), + mock.call("claim_in_str"), ] - assert str(e.value) == 'DID token is malformed. Given claim should be ' \ - 'a JSON serialized string. Exception ().' + assert ( + str(e.value) == "DID token is malformed. Given claim should be " + "a JSON serialized string. Exception ()." + ) def test_decode_passes(self, setup_mocks): setup_mocks.json_loads.side_effect = [ - ('proof_in_str', 'claim_in_str'), - 'claim', + ("proof_in_str", "claim_in_str"), + "claim", ] with mock.patch.object( Token, - '_check_required_fields', + "_check_required_fields", ) as mock_check_required_fields: - assert Token.decode(self.did_token) == ('proof_in_str', 'claim') + assert Token.decode(self.did_token) == ("proof_in_str", "claim") setup_mocks.urlsafe_b64decode.assert_called_once_with(self.did_token) - mock_check_required_fields.assert_called_once_with('claim') + mock_check_required_fields.assert_called_once_with("claim") assert setup_mocks.json_loads.call_args_list == [ mock.call(setup_mocks.urlsafe_b64decode.return_value.decode.return_value), - mock.call('claim_in_str'), + mock.call("claim_in_str"), ] class TestTokenValidate: - - did_token = 'magic_token' - public_address = 'magic_address' + did_token = "magic_token" + public_address = "magic_address" mock_funcs = namedtuple( - 'mock_funcs', + "mock_funcs", [ - 'proof', - 'claim', - 'decode', - 'recoverHash', - 'defunct_hash_message', - 'get_public_address', - 'epoch_time_now', - 'apply_did_token_nbf_grace_period', + "proof", + "claim", + "decode", + "recoverHash", + "defunct_hash_message", + "get_public_address", + "epoch_time_now", + "apply_did_token_nbf_grace_period", ], ) @pytest.fixture def setup_mocks(self): - proof = 'proof' + proof = "proof" claim = { - 'ext': 8084, - 'nbf': 6666, - 'aud': '1234', + "ext": 8084, + "nbf": 6666, + "aud": "1234", } - with mock.patch.object( - Token, - 'decode', - return_value=(proof, claim), - ) as decode, mock.patch( - 'magic_admin.resources.token.w3.eth.account.recover_message', - return_value=self.public_address, - ) as recoverHash, mock.patch( - 'magic_admin.resources.token.encode_defunct', - ) as defunct_hash_message, mock.patch.object( - Token, - 'get_public_address', - return_value=self.public_address, - ) as get_public_address, mock.patch( - 'magic_admin.resources.token.epoch_time_now', - return_value=claim['ext'] - 1, - ) as epoch_time_now, mock.patch( - 'magic_admin.resources.token.apply_did_token_nbf_grace_period', - return_value=claim['nbf'], - ) as apply_did_token_nbf_grace_period, mock.patch( - 'magic_admin.resources.token.magic_admin', - new=stub(client_id='1234'), + with ( + mock.patch.object( + Token, + "decode", + return_value=(proof, claim), + ) as decode, + mock.patch( + "magic_admin.resources.token.w3.eth.account.recover_message", + return_value=self.public_address, + ) as recoverHash, + mock.patch( + "magic_admin.resources.token.encode_defunct", + ) as defunct_hash_message, + mock.patch.object( + Token, + "get_public_address", + return_value=self.public_address, + ) as get_public_address, + mock.patch( + "magic_admin.resources.token.epoch_time_now", + return_value=claim["ext"] - 1, + ) as epoch_time_now, + mock.patch( + "magic_admin.resources.token.apply_did_token_nbf_grace_period", + return_value=claim["nbf"], + ) as apply_did_token_nbf_grace_period, + mock.patch( + "magic_admin.resources.token.magic_admin", + new=stub(client_id="1234"), + ), ): yield self.mock_funcs( proof, @@ -210,7 +229,7 @@ def _assert_validate_funcs_called( ): setup_mocks.decode.assert_called_once_with(self.did_token) setup_mocks.defunct_hash_message.assert_called_once_with( - text=json.dumps(setup_mocks.claim, separators=(',', ':')), + text=json.dumps(setup_mocks.claim, separators=(",", ":")), ) setup_mocks.recoverHash.assert_called_once_with( setup_mocks.defunct_hash_message.return_value, @@ -227,24 +246,25 @@ def _assert_validate_funcs_called( if is_grace_period_func_called: setup_mocks.apply_did_token_nbf_grace_period.assert_called_once_with( - setup_mocks.claim['nbf'], + setup_mocks.claim["nbf"], ) else: setup_mocks.apply_did_token_nbf_grace_period.assert_not_called() def test_validate_raises_error_if_signature_mismatch(self, setup_mocks): - setup_mocks.get_public_address.return_value = 'random_public_address' + setup_mocks.get_public_address.return_value = "random_public_address" with pytest.raises(DIDTokenInvalid) as e: Token.validate(self.did_token) self._assert_validate_funcs_called(setup_mocks) - assert str(e.value) == 'Signature mismatch between "proof" and "claim". ' \ - 'Please generate a new token with an intended issuer.' + assert ( + str(e.value) == 'Signature mismatch between "proof" and "claim". ' + "Please generate a new token with an intended issuer." + ) def test_validate_raises_error_if_did_token_expires(self, setup_mocks): - setup_mocks.epoch_time_now.return_value = \ - setup_mocks.claim['ext'] + 1 + setup_mocks.epoch_time_now.return_value = setup_mocks.claim["ext"] + 1 with pytest.raises(DIDTokenExpired) as e: Token.validate(self.did_token) @@ -253,21 +273,21 @@ def test_validate_raises_error_if_did_token_expires(self, setup_mocks): setup_mocks, is_time_func_called=True, ) - assert str(e.value) == 'Given DID token has expired. Please generate a ' \ - 'new one.' + assert str(e.value) == "Given DID token has expired. Please generate a new one." def test_validate_raises_error_if_did_token_has_no_expiration(self, setup_mocks): - setup_mocks.claim['ext'] = None + setup_mocks.claim["ext"] = None with pytest.raises(DIDTokenInvalid) as e: Token.validate(self.did_token) - assert str(e.value) == 'Please check the "ext" field and regenerate a new' \ - ' token with a suitable value.' + assert ( + str(e.value) == 'Please check the "ext" field and regenerate a new' + " token with a suitable value." + ) def test_validate_raises_error_if_did_token_used_before_nbf(self, setup_mocks): - setup_mocks.epoch_time_now.return_value = \ - setup_mocks.claim['nbf'] - 1 + setup_mocks.epoch_time_now.return_value = setup_mocks.claim["nbf"] - 1 with pytest.raises(DIDTokenInvalid) as e: Token.validate(self.did_token) @@ -277,9 +297,11 @@ def test_validate_raises_error_if_did_token_used_before_nbf(self, setup_mocks): is_time_func_called=True, is_grace_period_func_called=True, ) - assert str(e.value) == 'Given DID token cannot be used at this time. ' \ - 'Please check the "nbf" field and regenerate a new token with a ' \ - 'suitable value.' + assert ( + str(e.value) == "Given DID token cannot be used at this time. " + 'Please check the "nbf" field and regenerate a new token with a ' + "suitable value." + ) def test_validate_passes(self, setup_mocks): Token.validate(self.did_token) diff --git a/tests/unit/resources/user_test.py b/tests/unit/resources/user_test.py index 0d0c507..e336572 100644 --- a/tests/unit/resources/user_test.py +++ b/tests/unit/resources/user_test.py @@ -11,7 +11,6 @@ class TestUser: - metadata_with_wallets = stub( data=stub( email=sentinel.email, @@ -59,7 +58,7 @@ def setup(self): @pytest.fixture def mock_construct_issuer_with_public_address(self, mocker): return mocker.patch( - 'magic_admin.resources.user.construct_issuer_with_public_address', + "magic_admin.resources.user.construct_issuer_with_public_address", return_value=sentinel.public_address, ) @@ -68,9 +67,12 @@ def test_get_metadata_by_issuer(self): return_value=self.metadata_no_wallets, ) - assert self.user.get_metadata_by_issuer( - sentinel.issuer, - ) == self.metadata_no_wallets + assert ( + self.user.get_metadata_by_issuer( + sentinel.issuer, + ) + == self.metadata_no_wallets + ) self.user.get_metadata_by_issuer_and_wallet.assert_called_once_with( sentinel.issuer, @@ -80,26 +82,34 @@ def test_get_metadata_by_issuer(self): def test_get_metadata_by_issuer_and_any_wallet(self): self.user.request = mock.Mock(return_value=self.metadata_with_wallets) - assert self.user.get_metadata_by_issuer_and_wallet( - sentinel.issuer, - WalletType.ANY, - ) == self.metadata_with_wallets + assert ( + self.user.get_metadata_by_issuer_and_wallet( + sentinel.issuer, + WalletType.ANY, + ) + == self.metadata_with_wallets + ) self.user.request.assert_called_once_with( - 'get', + "get", self.user.v1_user_info, params={ - 'issuer': sentinel.issuer, - 'wallet_type': WalletType.ANY, + "issuer": sentinel.issuer, + "wallet_type": WalletType.ANY, }, ) def test_get_metadata_by_token(self): - self.user.get_metadata_by_issuer = mock.Mock(return_value=self.metadata_no_wallets) + self.user.get_metadata_by_issuer = mock.Mock( + return_value=self.metadata_no_wallets + ) - assert self.user.get_metadata_by_token( - future_did_token, - ) == self.user.get_metadata_by_issuer.return_value + assert ( + self.user.get_metadata_by_token( + future_did_token, + ) + == self.user.get_metadata_by_issuer.return_value + ) self.user.Token.get_issuer.assert_called_once_with(future_did_token) self.user.get_metadata_by_issuer.assert_called_once_with( @@ -111,10 +121,13 @@ def test_get_metadata_by_token_and_any_wallet(self): return_value=self.metadata_with_wallets, ) - assert self.user.get_metadata_by_token_and_wallet( - future_did_token, - WalletType.ANY, - ) == self.user.get_metadata_by_issuer_and_wallet.return_value + assert ( + self.user.get_metadata_by_token_and_wallet( + future_did_token, + WalletType.ANY, + ) + == self.user.get_metadata_by_issuer_and_wallet.return_value + ) self.user.Token.get_issuer.assert_called_once_with(future_did_token) self.user.get_metadata_by_issuer_and_wallet.assert_called_once_with( @@ -126,11 +139,16 @@ def test_get_metadata_by_public_address( self, mock_construct_issuer_with_public_address, ): - self.user.get_metadata_by_issuer = mock.Mock(return_value=self.metadata_no_wallets) + self.user.get_metadata_by_issuer = mock.Mock( + return_value=self.metadata_no_wallets + ) - assert self.user.get_metadata_by_public_address( - sentinel.public_address, - ) == self.user.get_metadata_by_issuer.return_value + assert ( + self.user.get_metadata_by_public_address( + sentinel.public_address, + ) + == self.user.get_metadata_by_issuer.return_value + ) mock_construct_issuer_with_public_address.assert_called_once_with( sentinel.public_address, @@ -147,10 +165,13 @@ def test_get_metadata_by_public_address_and_any_wallet( return_value=self.metadata_with_wallets, ) - assert self.user.get_metadata_by_public_address_and_wallet( - sentinel.public_address, - WalletType.ANY, - ) == self.user.get_metadata_by_issuer_and_wallet.return_value + assert ( + self.user.get_metadata_by_public_address_and_wallet( + sentinel.public_address, + WalletType.ANY, + ) + == self.user.get_metadata_by_issuer_and_wallet.return_value + ) mock_construct_issuer_with_public_address.assert_called_once_with( sentinel.public_address, @@ -168,10 +189,10 @@ def test_logout_by_issuer(self): ) self.user.request.assert_called_once_with( - 'post', - self.user.v2_user_logout, + "post", + self.user.v1_user_logout, data={ - 'issuer': sentinel.issuer, + "issuer": sentinel.issuer, }, ) @@ -181,9 +202,12 @@ def test_logout_by_public_address( ): self.user.logout_by_issuer = mock.Mock() - assert self.user.logout_by_public_address( - public_address, - ) == self.user.logout_by_issuer.return_value + assert ( + self.user.logout_by_public_address( + public_address, + ) + == self.user.logout_by_issuer.return_value + ) mock_construct_issuer_with_public_address.assert_called_once_with( public_address, @@ -195,9 +219,12 @@ def test_logout_by_public_address( def test_logout_by_token(self): self.user.logout_by_issuer = mock.Mock() - assert self.user.logout_by_token( - future_did_token, - ) == self.user.logout_by_issuer.return_value + assert ( + self.user.logout_by_token( + future_did_token, + ) + == self.user.logout_by_issuer.return_value + ) self.user.Token.get_issuer.assert_called_once_with(future_did_token) self.user.logout_by_issuer.assert_called_once_with( diff --git a/tests/unit/response_test.py b/tests/unit/response_test.py index c2f24ae..2e87c0b 100644 --- a/tests/unit/response_test.py +++ b/tests/unit/response_test.py @@ -2,10 +2,9 @@ class TestMagicResponse: - - content = 'troll_goat' + content = "troll_goat" status_code = 200 - resp_data = {'data': 'another_troll_goat'} + resp_data = {"data": "another_troll_goat"} def test_response(self): resp = MagicResponse( @@ -16,4 +15,4 @@ def test_response(self): assert resp.content == self.content assert resp.status_code == self.status_code - assert resp.data == self.resp_data['data'] + assert resp.data == self.resp_data diff --git a/tests/unit/utils/did_token_test.py b/tests/unit/utils/did_token_test.py index 2e02755..9dbaf80 100644 --- a/tests/unit/utils/did_token_test.py +++ b/tests/unit/utils/did_token_test.py @@ -8,8 +8,7 @@ class TestDIDToken: - - malformed_issuer = 'troll_goat' + malformed_issuer = "troll_goat" def test_parse_public_address_from_issuer(self): assert parse_public_address_from_issuer(issuer) == public_address @@ -18,9 +17,11 @@ def test_parse_public_address_from_issuer_raises_error(self): with pytest.raises(DIDTokenMalformed) as e: parse_public_address_from_issuer(self.malformed_issuer) - assert str(e.value) == \ - 'Given issuer ({}) is malformed. Please make sure it follows the ' \ - '`did:method-name:method-specific-id` format.'.format(self.malformed_issuer) + assert ( + str(e.value) + == "Given issuer ({}) is malformed. Please make sure it follows the " + "`did:method-name:method-specific-id` format.".format(self.malformed_issuer) + ) def test_construct_issuer_with_public_address(self): assert issuer == construct_issuer_with_public_address(public_address) diff --git a/tests/unit/utils/http_test.py b/tests/unit/utils/http_test.py index 90f32de..241390c 100644 --- a/tests/unit/utils/http_test.py +++ b/tests/unit/utils/http_test.py @@ -5,24 +5,22 @@ class TestNullSafe: - - @pytest.mark.parametrize('value', [None, 'null', 'none', 'None', '']) + @pytest.mark.parametrize("value", [None, "null", "none", "None", ""]) def test_returns_none(self, value): assert null_safe(value) is None def test_returns_value(self): - value = 'troll_goat' + value = "troll_goat" assert null_safe(value) == value class TestParseAuthHeaderValue: - - malformed = 'wrong_format' - expected = 'Bearer troll_goat' + malformed = "wrong_format" + expected = "Bearer troll_goat" def test_returns_none_if_not_in_bearer_format(self): assert parse_authorization_header_value(self.malformed) is None def test_returns_value(self): - assert parse_authorization_header_value(self.expected) == 'troll_goat' + assert parse_authorization_header_value(self.expected) == "troll_goat" diff --git a/tests/unit/utils/time_test.py b/tests/unit/utils/time_test.py index d353ec1..2aef0e1 100644 --- a/tests/unit/utils/time_test.py +++ b/tests/unit/utils/time_test.py @@ -6,9 +6,8 @@ class TestTimeUtils: - def test_epoch_time_now(self): - with mock.patch('magic_admin.utils.time.time') as mock_time: + with mock.patch("magic_admin.utils.time.time") as mock_time: mock_time.time.return_value = 8084 assert epoch_time_now() == 8084 @@ -17,6 +16,9 @@ def test_epoch_time_now(self): def test_apply_did_token_nbf_grace_period(self): timestamp = 8084 - assert apply_did_token_nbf_grace_period( - timestamp, - ) == timestamp - did_token_nbf_grace_period_s + assert ( + apply_did_token_nbf_grace_period( + timestamp, + ) + == timestamp - did_token_nbf_grace_period_s + ) diff --git a/tox.ini b/tox.ini index 8d85e3a..ffbd601 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py311,py39,py310 +envlist = py311,py312,py313 skipsdist=True [testenv]