├── .github └── workflows │ ├── lint.yml │ ├── publish.yml │ ├── push.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── jsonstar ├── __init__.py ├── decoder.py ├── default_encoders.py ├── encoder.py └── null_dict.py ├── lint.sh ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── test_default_encoder.py ├── test_encoder.py └── test_register_defaults.py /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | linting: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-python@v5 12 | with: 13 | python-version: "3.12" 14 | - name: Install dependencies 15 | run: | 16 | python -m pip install --upgrade pip poetry 17 | poetry install 18 | - name: Lint 19 | run: | 20 | poetry run pre-commit run --all-files 21 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | lint: 9 | uses: ./.github/workflows/lint.yml 10 | 11 | test: 12 | uses: ./.github/workflows/test.yml 13 | 14 | pypi-publish: 15 | name: Publish release to PyPI 16 | runs-on: ubuntu-latest 17 | needs: [lint, test] 18 | environment: 19 | name: pypi 20 | url: https://pypi.org/p/jsonstar 21 | permissions: 22 | id-token: write 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.12" 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip poetry 31 | poetry install 32 | - name: Build package 33 | run: | 34 | python -m build 35 | - name: Publish package distributions to PyPI 36 | uses: pypa/gh-action-pypi-publish@release/v1 37 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | uses: ./.github/workflows/lint.yml 8 | 9 | test: 10 | needs: lint 11 | uses: ./.github/workflows/test.yml 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | testing: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.9", "3.10", "3.11", "3.12"] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-python@v5 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip poetry 21 | poetry install 22 | - name: Test 23 | run: poetry run pytest 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | ### JetBrains template 142 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 143 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 144 | 145 | # User-specific stuff 146 | .idea 147 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | # https://pre-commit.com/#meta-hooks 5 | - repo: meta 6 | hooks: 7 | - id: check-useless-excludes 8 | 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v4.5.0 11 | hooks: 12 | - id: check-added-large-files 13 | - id: check-ast 14 | - id: check-case-conflict 15 | - id: check-executables-have-shebangs 16 | - id: check-merge-conflict 17 | - id: check-shebang-scripts-are-executable 18 | - id: check-symlinks 19 | - id: check-toml 20 | - id: check-yaml 21 | - id: debug-statements 22 | - id: destroyed-symlinks 23 | - id: end-of-file-fixer 24 | - id: fix-byte-order-marker 25 | - id: fix-encoding-pragma 26 | args: 27 | - --remove 28 | - id: forbid-new-submodules 29 | always_run: true 30 | - id: mixed-line-ending 31 | - id: trailing-whitespace 32 | 33 | - repo: local 34 | hooks: 35 | # https://black.readthedocs.io/en/stable/ 36 | # https://github.com/psf/black/blob/main/.pre-commit-hooks.yaml 37 | - id: black 38 | name: black (local) 39 | description: Run black code formatter 40 | entry: python -m black 41 | language: system 42 | require_serial: true 43 | types_or: 44 | - python 45 | - pyi 46 | 47 | # https://pycqa.github.io/isort/ 48 | # https://github.com/PyCQA/isort/blob/main/.pre-commit-hooks.yaml 49 | - id: isort 50 | name: isort (local) 51 | description: Run isort import formatter 52 | entry: python -m isort 53 | language: system 54 | require_serial: true 55 | types_or: 56 | - python 57 | - pyi 58 | args: 59 | - --jobs=-1 60 | - --python-version=auto 61 | - --settings=pyproject.toml 62 | 63 | # https://flake8.pycqa.org/en/latest/ 64 | # https://github.com/PyCQA/flake8/blob/main/.pre-commit-hooks.yaml 65 | - id: flake8 66 | name: flake8 (local) 67 | description: Run flake8 style linter 68 | entry: python -m flake8 69 | language: system 70 | require_serial: true 71 | types_or: 72 | - python 73 | - pyi 74 | args: 75 | - --max-line-length=120 76 | 77 | # https://python-poetry.org/docs/pre-commit-hooks/ 78 | # https://github.com/python-poetry/poetry/blob/master/.pre-commit-hooks.yaml 79 | - id: poetry-check 80 | name: poetry check (local) 81 | description: Ensure pyproject.toml file is valid 82 | entry: poetry check -n 83 | language: system 84 | pass_filenames: false 85 | files: ^pyproject\.toml$ 86 | 87 | - id: poetry-lock 88 | name: poetry lock (local) 89 | description: Ensure lock file is up-to-date 90 | entry: bash -c 'poetry check --lock -n || poetry lock -n --no-update' 91 | language: system 92 | pass_filenames: false 93 | files: ^(pyproject\.toml|poetry\.lock)$ 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Henrique Bastos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON* is an extensible json module to serialize all objects! 2 | 3 | `jsonstar` extends Python's standard JSON encoder and decoder to easily handle your custom types. 4 | 5 | This means you won't have to transform your custom types into dictionaries with primitive types before encoding them to JSON. And you won't have to parse back the encoded strings into your custom types after decoding them from JSON. 6 | 7 | ## How to install it? 8 | 9 | ```bash 10 | pip install jsonstar 11 | ``` 12 | 13 | ## How to start using it? 14 | 15 | The `jsonstar` module provides the same API as the standard `json` module, so you can use it as a drop in replacement. 16 | 17 | Simply change your import from `import json` to `import jsonstar as json` and you're good to go. 18 | 19 | ## Why use it? 20 | 21 | Consider you have a pydantic `Employee` class that you want to serialize to JSON. 22 | 23 | ```python 24 | from decimal import Decimal 25 | from datetime import date 26 | from pydantic import BaseModel 27 | 28 | class Employee(BaseModel): 29 | name: str 30 | salary: Decimal 31 | birthday: date 32 | roles: set 33 | 34 | employee = Employee( 35 | name="John Doe", 36 | salary=Decimal("1000.00"), 37 | birthday=date(1990, 1, 1), 38 | roles={"A", "B", "C"}, 39 | ) 40 | ``` 41 | 42 | The standard `json` module can't serialize the `employee` instance, requiring you to call its `dict` method. 43 | This will not suffice, because the standard `json` module don't know how to encode `Decimal`, `date` and `set`. 44 | Your solution would include some transfomation of the `employee` instance and its attributes before encoding it to JSON. 45 | 46 | That is where `jsonstar` shines by providing default encoder for common types like `pydantic.BaseModel`, 47 | `decimal.Decimal`, `datetime.date` and `set`. And allowing you to easily add your own encoders. 48 | 49 | ```python 50 | from jsonstar as json 51 | print(json.dumps(employee)) 52 | # {"name": "John Doe", "salary": "1000.00", "birthday": "1990-01-01", "roles": ["A", "B", "C"]} 53 | ``` 54 | 55 | ## What default encoders are provided? 56 | 57 | By default, `jsonstar` provides encoders for the following types: 58 | 59 | - `attrs` classes 60 | - `dataclasses.dataclass` classes 61 | - `datetime.date` 62 | - `datetime.datetime` 63 | - `datetime.time` 64 | - `datetime.timedelta` 65 | - `decimal.Decimal` 66 | - `frozenset` 67 | - `pydantic.BaseModel` 68 | - `set` 69 | - `uuid.UUID` 70 | 71 | ### Can `jsonstar` add more default encoders? 72 | 73 | Yes. If you think that a default encoder for a common type is missing, please open an issue or a pull request. 74 | See the *How to contribute* section for more details. 75 | 76 | ## How do I add my own encoder? 77 | 78 | First you need to decide where you want your encoder to be available: 79 | 80 | 1. *Class default encoders* happen when your `MyEncoder` class inherits from `JSONEncoderStar` and you add encoders to it. 81 | 2. *Library-wide default encoder* are added directly to `JSONEncoderStar` class and is available everywhere in your project. 82 | 83 | Also you have two types of encoders to choose from: 84 | 85 | - *Typed encoders* are used to encode a specific type identified by `isinstance`. 86 | - *Functional encoders* are used to encode an object based on arbitraty logic. 87 | 88 | *Note:* From experience we find that *class encoders* are the most common use case. 89 | 90 | ### How to add class default encoders? 91 | 92 | ```python 93 | import jsonstar as json 94 | from decimal import Decimal 95 | from datetime import date 96 | 97 | 98 | # You can declare it on the special class attributes 99 | class MyEncoder(json.JSONEncoderStar): 100 | _default_typed_encoders = {Decimal: lambda o: str(o.quantize(Decimal("1.00")))} 101 | 102 | # Or you can register it after the class is declared 103 | MyEncoder.register_default_encoder(lambda o: o.strftime("%Y-%m-%d"), date) 104 | ``` 105 | 106 | ### How to add a library-wide default encoder? 107 | 108 | ```python 109 | import jsonstar as json 110 | from decimal import Decimal 111 | 112 | 113 | def two_decimals_encoder(obj): 114 | """Encodes a decimal with only two decimal places.""" 115 | return str(obj.quantize(Decimal("1.00"))) 116 | 117 | 118 | json.register_default_encoder(Decimal, two_decimals_encoder) 119 | ``` 120 | 121 | ### How to add a typed encoder? 122 | 123 | *Typed encoders* are specific to a type and it's inherited types. 124 | 125 | When registering a typed encoder, you simply pass the encoder and the type to the chosen registration method. 126 | 127 | When you add a typed encoder, `jsonstar` will check if any base class already has a registered encoder make sure the more generic encoder is used last, respecting Python's Method Resolution Order (MRO). 128 | 129 | ### How to add a functional encoder? 130 | 131 | *Functional encoders* are used to encode an object based on arbitraty logic and not specific to a type. 132 | 133 | To register a functional encoder, you simply pass the encoder to the chosen registration method omiting the type. 134 | 135 | All functional encoders are called only for objects that do not have a registered typed encoder. 136 | 137 | ## Contributing 138 | 139 | Pull requests are welcome and must have associated tests. 140 | 141 | For major changes, please open an issue first to discuss what you would like to change. 142 | 143 | ## License 144 | 145 | [MIT](https://choosealicense.com/licenses/mit/) 146 | 147 | ## Author 148 | 149 | Henrique Bastos 150 | 151 | ## Project links 152 | 153 | - [Homepage](https://github.com/henriquebastos/python-jsonstar) 154 | - [Repository](https://github.com/henriquebastos/python-jsonstar) 155 | - [Documentation](https://github.com/henriquebastos/python-jsonstar) 156 | -------------------------------------------------------------------------------- /jsonstar/__init__.py: -------------------------------------------------------------------------------- 1 | import json as stdlib_json 2 | 3 | from jsonstar.decoder import JSONDecoderStar 4 | from jsonstar.encoder import JSONEncoderStar 5 | 6 | 7 | def dump(obj, fp, cls=JSONDecoderStar, **kwargs): 8 | return stdlib_json.dump(obj, fp, cls=cls, **kwargs) 9 | 10 | 11 | def dumps(obj, cls=JSONEncoderStar, **kwargs): 12 | return stdlib_json.dumps(obj, cls=cls, **kwargs) 13 | 14 | 15 | def load(fp, *, cls=None, **kwargs): 16 | return stdlib_json.load(fp, cls=cls, **kwargs) 17 | 18 | 19 | def loads(obj, cls=JSONDecoderStar, **kwargs): 20 | return stdlib_json.loads(obj, cls=cls, **kwargs) 21 | 22 | 23 | def register_default_encoder(function, type_=JSONEncoderStar.FUNCTIONAL): 24 | return JSONEncoderStar.register_default_encoder(function, type_) 25 | -------------------------------------------------------------------------------- /jsonstar/decoder.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json as stdlib_json 3 | 4 | 5 | class JSONDecoderStar(stdlib_json.JSONDecoder): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(object_hook=self.hook, *args, **kwargs) 8 | 9 | @staticmethod 10 | def hook(source): 11 | d = {} 12 | for k, v in source.items(): 13 | if isinstance(v, str) and not v.isdigit(): 14 | try: 15 | d[k] = datetime.datetime.fromisoformat(v) 16 | except (ValueError, TypeError): 17 | d[k] = v 18 | else: 19 | d[k] = v 20 | 21 | return d 22 | -------------------------------------------------------------------------------- /jsonstar/default_encoders.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import datetime 3 | import decimal 4 | import uuid 5 | 6 | 7 | try: 8 | from django.db.models import Model 9 | from django.forms.models import model_to_dict 10 | 11 | DJANGO_TYPED_ENCODERS = { 12 | Model: model_to_dict, 13 | } 14 | except ImportError: 15 | DJANGO_TYPED_ENCODERS = {} 16 | 17 | 18 | try: 19 | from pydantic import BaseModel 20 | 21 | def pydantic_dict(o): 22 | return o.dict() 23 | 24 | PYDANTIC_TYPED_ENCODERS = { 25 | BaseModel: pydantic_dict, 26 | } 27 | except ImportError: 28 | PYDANTIC_TYPED_ENCODERS = {} 29 | 30 | 31 | try: 32 | import attrs 33 | 34 | def attrs_dict(o): 35 | return attrs.asdict(o) 36 | 37 | ATTRS_FUNCTIONAL_ENCODERS = [attrs_dict] 38 | except ImportError: 39 | ATTRS_FUNCTIONAL_ENCODERS = () 40 | 41 | 42 | def encode_timedelta_as_iso_string(duration): 43 | sign = "-" if duration < datetime.timedelta(0) else "" 44 | duration = abs(duration) 45 | total_seconds = int(duration.total_seconds()) 46 | days, remainder = divmod(total_seconds, 86400) 47 | hours, remainder = divmod(remainder, 3600) 48 | minutes, seconds = divmod(remainder, 60) 49 | ms = f".{duration.microseconds:06d}" if duration.microseconds else "" 50 | return f"{sign}P{days}DT{hours:02d}H{minutes:02d}M{seconds:02d}{ms}S" 51 | 52 | 53 | def dataclasses_asdict(o): 54 | return dataclasses.asdict(o) 55 | 56 | 57 | DEFAULT_FUNCTIONAL_ENCODERS = [ 58 | dataclasses_asdict, 59 | *ATTRS_FUNCTIONAL_ENCODERS, 60 | ] 61 | 62 | DEFAULT_TYPED_ENCODERS = { 63 | datetime.datetime: lambda o: o.isoformat(timespec="milliseconds").replace("+00:00", "Z"), 64 | datetime.date: lambda o: o.isoformat(), 65 | datetime.time: lambda o: o.isoformat(timespec="milliseconds"), 66 | datetime.timedelta: encode_timedelta_as_iso_string, 67 | decimal.Decimal: str, 68 | uuid.UUID: str, 69 | set: list, 70 | frozenset: list, 71 | **DJANGO_TYPED_ENCODERS, 72 | **PYDANTIC_TYPED_ENCODERS, 73 | } 74 | -------------------------------------------------------------------------------- /jsonstar/encoder.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json as stdlib_json 3 | from collections import ChainMap, OrderedDict 4 | from contextlib import suppress 5 | from itertools import chain 6 | 7 | from jsonstar.default_encoders import DEFAULT_FUNCTIONAL_ENCODERS, DEFAULT_TYPED_ENCODERS 8 | from jsonstar.null_dict import NULL_DICT 9 | 10 | 11 | __all__ = ["JSONEncoderStar"] 12 | 13 | 14 | class TypedEncoderRegistry(OrderedDict): 15 | def __setitem__(self, type_, function): 16 | """Register the type encoder and ensures inherited takes precedence over its base types encoders.""" 17 | super().__setitem__(type_, function) 18 | for base in inspect.getmro(type_): 19 | if base in self: 20 | self.move_to_end(base) 21 | 22 | 23 | class EncoderMeta(type): 24 | def __new__(mcs, name, bases, namespace): 25 | if "_default_functional_encoders" not in namespace: 26 | namespace["_default_functional_encoders"] = [] 27 | 28 | if "_default_typed_encoders" not in namespace: 29 | namespace["_default_typed_encoders"] = TypedEncoderRegistry() 30 | 31 | return super().__new__(mcs, name, bases, namespace) 32 | 33 | 34 | class JSONEncoderStar(stdlib_json.JSONEncoder, metaclass=EncoderMeta): 35 | class FUNCTIONAL: 36 | """Sentinel type to register a functional encoder.""" 37 | 38 | _default_typed_encoders = TypedEncoderRegistry(DEFAULT_TYPED_ENCODERS) 39 | _default_functional_encoders = DEFAULT_FUNCTIONAL_ENCODERS 40 | 41 | def __init__(self, *args, functional_encoders=(), typed_encoders: dict[type, callable] = NULL_DICT, **kwargs): 42 | super().__init__(*args, **kwargs) 43 | 44 | self._typed_encoders = TypedEncoderRegistry(typed_encoders) 45 | self._functional_encoders = [*functional_encoders] 46 | 47 | @classmethod 48 | def default_functional_encoders(cls): 49 | if cls is JSONEncoderStar: 50 | return cls._default_functional_encoders 51 | else: 52 | return cls._default_functional_encoders + cls.__base__.default_functional_encoders() 53 | 54 | @classmethod 55 | def default_typed_encoders(cls): 56 | if cls is JSONEncoderStar: 57 | return cls._default_typed_encoders 58 | else: 59 | return TypedEncoderRegistry(ChainMap(cls._default_typed_encoders, cls.__base__.default_typed_encoders())) 60 | 61 | @property 62 | def functional_encoders(self): 63 | return chain(self._functional_encoders, self.default_functional_encoders()) 64 | 65 | @property 66 | def typed_encoders(self): 67 | i = self._typed_encoders 68 | base = self.default_typed_encoders() 69 | return ChainMap(i, base) 70 | 71 | def register(self, function, type_=FUNCTIONAL): 72 | if type_ is self.FUNCTIONAL: 73 | self._functional_encoders.append(function) 74 | else: 75 | self._typed_encoders[type_] = function 76 | 77 | @classmethod 78 | def register_default_encoder(cls, function, type_=FUNCTIONAL): 79 | if type_ is cls.FUNCTIONAL: 80 | cls._default_functional_encoders.append(function) 81 | else: 82 | cls._default_typed_encoders[type_] = function 83 | 84 | def default(self, o) -> str: 85 | for base, encoder in self.typed_encoders.items(): 86 | if isinstance(o, base): 87 | return encoder(o) 88 | 89 | for encoder in self.functional_encoders: 90 | with suppress(Exception): 91 | return encoder(o) 92 | 93 | return super().default(o) 94 | -------------------------------------------------------------------------------- /jsonstar/null_dict.py: -------------------------------------------------------------------------------- 1 | __all__ = ["NULL_DICT"] 2 | 3 | from collections.abc import Mapping 4 | 5 | 6 | class NullDict(Mapping): 7 | """A read-only empty dict-like class to be safely used as default argument.""" 8 | 9 | def __len__(self): 10 | return 0 11 | 12 | def __iter__(self): 13 | return iter([]) 14 | 15 | def __getitem__(self, key): 16 | return None 17 | 18 | def __setitem__(self, key, value): 19 | pass 20 | 21 | def __contains__(self, key): 22 | return False 23 | 24 | 25 | """A singleton instance of NullDict.""" 26 | 27 | NULL_DICT = NullDict() 28 | -------------------------------------------------------------------------------- /lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | source $VENV_BIN/activate 3 | pre-commit run isort --files $@ 4 | pre-commit run black --files $@ 5 | pre-commit run flake8 --files $@ 6 | exit $? 7 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.6.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, 12 | {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, 13 | ] 14 | 15 | [[package]] 16 | name = "asgiref" 17 | version = "3.7.2" 18 | description = "ASGI specs, helper code, and adapters" 19 | optional = false 20 | python-versions = ">=3.7" 21 | groups = ["dev"] 22 | files = [ 23 | {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, 24 | {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, 25 | ] 26 | 27 | [package.dependencies] 28 | typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} 29 | 30 | [package.extras] 31 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 32 | 33 | [[package]] 34 | name = "astroid" 35 | version = "3.0.3" 36 | description = "An abstract syntax tree for Python with inference support." 37 | optional = false 38 | python-versions = ">=3.8.0" 39 | groups = ["dev"] 40 | files = [ 41 | {file = "astroid-3.0.3-py3-none-any.whl", hash = "sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17"}, 42 | {file = "astroid-3.0.3.tar.gz", hash = "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93"}, 43 | ] 44 | 45 | [package.dependencies] 46 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} 47 | 48 | [[package]] 49 | name = "attrs" 50 | version = "23.2.0" 51 | description = "Classes Without Boilerplate" 52 | optional = false 53 | python-versions = ">=3.7" 54 | groups = ["dev"] 55 | files = [ 56 | {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, 57 | {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, 58 | ] 59 | 60 | [package.extras] 61 | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] 62 | dev = ["attrs[tests]", "pre-commit"] 63 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] 64 | tests = ["attrs[tests-no-zope]", "zope-interface"] 65 | tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] 66 | tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] 67 | 68 | [[package]] 69 | name = "black" 70 | version = "24.1.1" 71 | description = "The uncompromising code formatter." 72 | optional = false 73 | python-versions = ">=3.8" 74 | groups = ["dev"] 75 | files = [ 76 | {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, 77 | {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, 78 | {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, 79 | {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, 80 | {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, 81 | {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, 82 | {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, 83 | {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, 84 | {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, 85 | {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, 86 | {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, 87 | {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, 88 | {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, 89 | {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, 90 | {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, 91 | {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, 92 | {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, 93 | {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, 94 | {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, 95 | {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, 96 | {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, 97 | {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, 98 | ] 99 | 100 | [package.dependencies] 101 | click = ">=8.0.0" 102 | mypy-extensions = ">=0.4.3" 103 | packaging = ">=22.0" 104 | pathspec = ">=0.9.0" 105 | platformdirs = ">=2" 106 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 107 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 108 | 109 | [package.extras] 110 | colorama = ["colorama (>=0.4.3)"] 111 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 112 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 113 | uvloop = ["uvloop (>=0.15.2)"] 114 | 115 | [[package]] 116 | name = "build" 117 | version = "1.0.3" 118 | description = "A simple, correct Python build frontend" 119 | optional = false 120 | python-versions = ">= 3.7" 121 | groups = ["dev"] 122 | files = [ 123 | {file = "build-1.0.3-py3-none-any.whl", hash = "sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f"}, 124 | {file = "build-1.0.3.tar.gz", hash = "sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b"}, 125 | ] 126 | 127 | [package.dependencies] 128 | colorama = {version = "*", markers = "os_name == \"nt\""} 129 | importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} 130 | packaging = ">=19.0" 131 | pyproject_hooks = "*" 132 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 133 | 134 | [package.extras] 135 | docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] 136 | test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] 137 | typing = ["importlib-metadata (>=5.1)", "mypy (>=1.5.0,<1.6.0)", "tomli", "typing-extensions (>=3.7.4.3)"] 138 | virtualenv = ["virtualenv (>=20.0.35)"] 139 | 140 | [[package]] 141 | name = "cfgv" 142 | version = "3.4.0" 143 | description = "Validate configuration and produce human readable error messages." 144 | optional = false 145 | python-versions = ">=3.8" 146 | groups = ["dev"] 147 | files = [ 148 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 149 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 150 | ] 151 | 152 | [[package]] 153 | name = "click" 154 | version = "8.1.7" 155 | description = "Composable command line interface toolkit" 156 | optional = false 157 | python-versions = ">=3.7" 158 | groups = ["dev"] 159 | files = [ 160 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 161 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 162 | ] 163 | 164 | [package.dependencies] 165 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 166 | 167 | [[package]] 168 | name = "colorama" 169 | version = "0.4.6" 170 | description = "Cross-platform colored terminal text." 171 | optional = false 172 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 173 | groups = ["dev"] 174 | markers = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"" 175 | files = [ 176 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 177 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 178 | ] 179 | 180 | [[package]] 181 | name = "dill" 182 | version = "0.3.8" 183 | description = "serialize all of Python" 184 | optional = false 185 | python-versions = ">=3.8" 186 | groups = ["dev"] 187 | files = [ 188 | {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, 189 | {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, 190 | ] 191 | 192 | [package.extras] 193 | graph = ["objgraph (>=1.7.2)"] 194 | profile = ["gprof2dot (>=2022.7.29)"] 195 | 196 | [[package]] 197 | name = "distlib" 198 | version = "0.3.8" 199 | description = "Distribution utilities" 200 | optional = false 201 | python-versions = "*" 202 | groups = ["dev"] 203 | files = [ 204 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 205 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 206 | ] 207 | 208 | [[package]] 209 | name = "django" 210 | version = "4.2.10" 211 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 212 | optional = false 213 | python-versions = ">=3.8" 214 | groups = ["dev"] 215 | files = [ 216 | {file = "Django-4.2.10-py3-none-any.whl", hash = "sha256:a2d4c4d4ea0b6f0895acde632071aff6400bfc331228fc978b05452a0ff3e9f1"}, 217 | {file = "Django-4.2.10.tar.gz", hash = "sha256:b1260ed381b10a11753c73444408e19869f3241fc45c985cd55a30177c789d13"}, 218 | ] 219 | 220 | [package.dependencies] 221 | asgiref = ">=3.6.0,<4" 222 | sqlparse = ">=0.3.1" 223 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 224 | 225 | [package.extras] 226 | argon2 = ["argon2-cffi (>=19.1.0)"] 227 | bcrypt = ["bcrypt"] 228 | 229 | [[package]] 230 | name = "exceptiongroup" 231 | version = "1.2.0" 232 | description = "Backport of PEP 654 (exception groups)" 233 | optional = false 234 | python-versions = ">=3.7" 235 | groups = ["dev"] 236 | markers = "python_version < \"3.11\"" 237 | files = [ 238 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, 239 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, 240 | ] 241 | 242 | [package.extras] 243 | test = ["pytest (>=6)"] 244 | 245 | [[package]] 246 | name = "filelock" 247 | version = "3.13.1" 248 | description = "A platform independent file lock." 249 | optional = false 250 | python-versions = ">=3.8" 251 | groups = ["dev"] 252 | files = [ 253 | {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, 254 | {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, 255 | ] 256 | 257 | [package.extras] 258 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] 259 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 260 | typing = ["typing-extensions (>=4.8)"] 261 | 262 | [[package]] 263 | name = "flake8" 264 | version = "7.0.0" 265 | description = "the modular source code checker: pep8 pyflakes and co" 266 | optional = false 267 | python-versions = ">=3.8.1" 268 | groups = ["dev"] 269 | files = [ 270 | {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, 271 | {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, 272 | ] 273 | 274 | [package.dependencies] 275 | mccabe = ">=0.7.0,<0.8.0" 276 | pycodestyle = ">=2.11.0,<2.12.0" 277 | pyflakes = ">=3.2.0,<3.3.0" 278 | 279 | [[package]] 280 | name = "freezegun" 281 | version = "1.4.0" 282 | description = "Let your Python tests travel through time" 283 | optional = false 284 | python-versions = ">=3.7" 285 | groups = ["dev"] 286 | files = [ 287 | {file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"}, 288 | {file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"}, 289 | ] 290 | 291 | [package.dependencies] 292 | python-dateutil = ">=2.7" 293 | 294 | [[package]] 295 | name = "identify" 296 | version = "2.5.33" 297 | description = "File identification library for Python" 298 | optional = false 299 | python-versions = ">=3.8" 300 | groups = ["dev"] 301 | files = [ 302 | {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, 303 | {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, 304 | ] 305 | 306 | [package.extras] 307 | license = ["ukkonen"] 308 | 309 | [[package]] 310 | name = "importlib-metadata" 311 | version = "7.0.1" 312 | description = "Read metadata from Python packages" 313 | optional = false 314 | python-versions = ">=3.8" 315 | groups = ["dev"] 316 | markers = "python_version < \"3.10\"" 317 | files = [ 318 | {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, 319 | {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, 320 | ] 321 | 322 | [package.dependencies] 323 | zipp = ">=0.5" 324 | 325 | [package.extras] 326 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] 327 | perf = ["ipython"] 328 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] 329 | 330 | [[package]] 331 | name = "iniconfig" 332 | version = "2.0.0" 333 | description = "brain-dead simple config-ini parsing" 334 | optional = false 335 | python-versions = ">=3.7" 336 | groups = ["dev"] 337 | files = [ 338 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 339 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 340 | ] 341 | 342 | [[package]] 343 | name = "isort" 344 | version = "5.13.2" 345 | description = "A Python utility / library to sort Python imports." 346 | optional = false 347 | python-versions = ">=3.8.0" 348 | groups = ["dev"] 349 | files = [ 350 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 351 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 352 | ] 353 | 354 | [package.extras] 355 | colors = ["colorama (>=0.4.6)"] 356 | 357 | [[package]] 358 | name = "mccabe" 359 | version = "0.7.0" 360 | description = "McCabe checker, plugin for flake8" 361 | optional = false 362 | python-versions = ">=3.6" 363 | groups = ["dev"] 364 | files = [ 365 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 366 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 367 | ] 368 | 369 | [[package]] 370 | name = "mypy-extensions" 371 | version = "1.0.0" 372 | description = "Type system extensions for programs checked with the mypy type checker." 373 | optional = false 374 | python-versions = ">=3.5" 375 | groups = ["dev"] 376 | files = [ 377 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 378 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 379 | ] 380 | 381 | [[package]] 382 | name = "nodeenv" 383 | version = "1.8.0" 384 | description = "Node.js virtual environment builder" 385 | optional = false 386 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 387 | groups = ["dev"] 388 | files = [ 389 | {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, 390 | {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, 391 | ] 392 | 393 | [package.dependencies] 394 | setuptools = "*" 395 | 396 | [[package]] 397 | name = "packaging" 398 | version = "23.2" 399 | description = "Core utilities for Python packages" 400 | optional = false 401 | python-versions = ">=3.7" 402 | groups = ["dev"] 403 | files = [ 404 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 405 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 406 | ] 407 | 408 | [[package]] 409 | name = "pathspec" 410 | version = "0.12.1" 411 | description = "Utility library for gitignore style pattern matching of file paths." 412 | optional = false 413 | python-versions = ">=3.8" 414 | groups = ["dev"] 415 | files = [ 416 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 417 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 418 | ] 419 | 420 | [[package]] 421 | name = "platformdirs" 422 | version = "4.2.0" 423 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 424 | optional = false 425 | python-versions = ">=3.8" 426 | groups = ["dev"] 427 | files = [ 428 | {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, 429 | {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, 430 | ] 431 | 432 | [package.extras] 433 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 434 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 435 | 436 | [[package]] 437 | name = "pluggy" 438 | version = "1.4.0" 439 | description = "plugin and hook calling mechanisms for python" 440 | optional = false 441 | python-versions = ">=3.8" 442 | groups = ["dev"] 443 | files = [ 444 | {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, 445 | {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, 446 | ] 447 | 448 | [package.extras] 449 | dev = ["pre-commit", "tox"] 450 | testing = ["pytest", "pytest-benchmark"] 451 | 452 | [[package]] 453 | name = "pre-commit" 454 | version = "3.6.0" 455 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 456 | optional = false 457 | python-versions = ">=3.9" 458 | groups = ["dev"] 459 | files = [ 460 | {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, 461 | {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, 462 | ] 463 | 464 | [package.dependencies] 465 | cfgv = ">=2.0.0" 466 | identify = ">=1.0.0" 467 | nodeenv = ">=0.11.1" 468 | pyyaml = ">=5.1" 469 | virtualenv = ">=20.10.0" 470 | 471 | [[package]] 472 | name = "pycodestyle" 473 | version = "2.11.1" 474 | description = "Python style guide checker" 475 | optional = false 476 | python-versions = ">=3.8" 477 | groups = ["dev"] 478 | files = [ 479 | {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, 480 | {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, 481 | ] 482 | 483 | [[package]] 484 | name = "pydantic" 485 | version = "2.6.1" 486 | description = "Data validation using Python type hints" 487 | optional = false 488 | python-versions = ">=3.8" 489 | groups = ["dev"] 490 | files = [ 491 | {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, 492 | {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, 493 | ] 494 | 495 | [package.dependencies] 496 | annotated-types = ">=0.4.0" 497 | pydantic-core = "2.16.2" 498 | typing-extensions = ">=4.6.1" 499 | 500 | [package.extras] 501 | email = ["email-validator (>=2.0.0)"] 502 | 503 | [[package]] 504 | name = "pydantic-core" 505 | version = "2.16.2" 506 | description = "" 507 | optional = false 508 | python-versions = ">=3.8" 509 | groups = ["dev"] 510 | files = [ 511 | {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, 512 | {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, 513 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, 514 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, 515 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, 516 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, 517 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, 518 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, 519 | {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, 520 | {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, 521 | {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, 522 | {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, 523 | {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, 524 | {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, 525 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, 526 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, 527 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, 528 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, 529 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, 530 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, 531 | {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, 532 | {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, 533 | {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, 534 | {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, 535 | {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, 536 | {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, 537 | {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, 538 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, 539 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, 540 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, 541 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, 542 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, 543 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, 544 | {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, 545 | {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, 546 | {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, 547 | {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, 548 | {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, 549 | {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, 550 | {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, 551 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, 552 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, 553 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, 554 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, 555 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, 556 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, 557 | {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, 558 | {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, 559 | {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, 560 | {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, 561 | {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, 562 | {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, 563 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, 564 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, 565 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, 566 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, 567 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, 568 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, 569 | {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, 570 | {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, 571 | {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, 572 | {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, 573 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, 574 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, 575 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, 576 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, 577 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, 578 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, 579 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, 580 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, 581 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, 582 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, 583 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, 584 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, 585 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, 586 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, 587 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, 588 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, 589 | {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, 590 | ] 591 | 592 | [package.dependencies] 593 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 594 | 595 | [[package]] 596 | name = "pyflakes" 597 | version = "3.2.0" 598 | description = "passive checker of Python programs" 599 | optional = false 600 | python-versions = ">=3.8" 601 | groups = ["dev"] 602 | files = [ 603 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 604 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 605 | ] 606 | 607 | [[package]] 608 | name = "pylint" 609 | version = "3.0.3" 610 | description = "python code static checker" 611 | optional = false 612 | python-versions = ">=3.8.0" 613 | groups = ["dev"] 614 | files = [ 615 | {file = "pylint-3.0.3-py3-none-any.whl", hash = "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810"}, 616 | {file = "pylint-3.0.3.tar.gz", hash = "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b"}, 617 | ] 618 | 619 | [package.dependencies] 620 | astroid = ">=3.0.1,<=3.1.0-dev0" 621 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 622 | dill = [ 623 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 624 | {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, 625 | {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, 626 | ] 627 | isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" 628 | mccabe = ">=0.6,<0.8" 629 | platformdirs = ">=2.2.0" 630 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 631 | tomlkit = ">=0.10.1" 632 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 633 | 634 | [package.extras] 635 | spelling = ["pyenchant (>=3.2,<4.0)"] 636 | testutils = ["gitpython (>3)"] 637 | 638 | [[package]] 639 | name = "pyproject-hooks" 640 | version = "1.0.0" 641 | description = "Wrappers to call pyproject.toml-based build backend hooks." 642 | optional = false 643 | python-versions = ">=3.7" 644 | groups = ["dev"] 645 | files = [ 646 | {file = "pyproject_hooks-1.0.0-py3-none-any.whl", hash = "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8"}, 647 | {file = "pyproject_hooks-1.0.0.tar.gz", hash = "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"}, 648 | ] 649 | 650 | [package.dependencies] 651 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 652 | 653 | [[package]] 654 | name = "pytest" 655 | version = "8.0.0" 656 | description = "pytest: simple powerful testing with Python" 657 | optional = false 658 | python-versions = ">=3.8" 659 | groups = ["dev"] 660 | files = [ 661 | {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, 662 | {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, 663 | ] 664 | 665 | [package.dependencies] 666 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 667 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 668 | iniconfig = "*" 669 | packaging = "*" 670 | pluggy = ">=1.3.0,<2.0" 671 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 672 | 673 | [package.extras] 674 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 675 | 676 | [[package]] 677 | name = "python-dateutil" 678 | version = "2.8.2" 679 | description = "Extensions to the standard Python datetime module" 680 | optional = false 681 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 682 | groups = ["dev"] 683 | files = [ 684 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 685 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 686 | ] 687 | 688 | [package.dependencies] 689 | six = ">=1.5" 690 | 691 | [[package]] 692 | name = "pytz" 693 | version = "2024.1" 694 | description = "World timezone definitions, modern and historical" 695 | optional = false 696 | python-versions = "*" 697 | groups = ["dev"] 698 | files = [ 699 | {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, 700 | {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, 701 | ] 702 | 703 | [[package]] 704 | name = "pyyaml" 705 | version = "6.0.1" 706 | description = "YAML parser and emitter for Python" 707 | optional = false 708 | python-versions = ">=3.6" 709 | groups = ["dev"] 710 | files = [ 711 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 712 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 713 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 714 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 715 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 716 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 717 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 718 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 719 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 720 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 721 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 722 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 723 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 724 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 725 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 726 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 727 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 728 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 729 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 730 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 731 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 732 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 733 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 734 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 735 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 736 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 737 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 738 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 739 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 740 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 741 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 742 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 743 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 744 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 745 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 746 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 747 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 748 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 749 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 750 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 751 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 752 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 753 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 754 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 755 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 756 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 757 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 758 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 759 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 760 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 761 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 762 | ] 763 | 764 | [[package]] 765 | name = "setuptools" 766 | version = "69.0.3" 767 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 768 | optional = false 769 | python-versions = ">=3.8" 770 | groups = ["dev"] 771 | files = [ 772 | {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, 773 | {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, 774 | ] 775 | 776 | [package.extras] 777 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 778 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 779 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 780 | 781 | [[package]] 782 | name = "six" 783 | version = "1.16.0" 784 | description = "Python 2 and 3 compatibility utilities" 785 | optional = false 786 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 787 | groups = ["dev"] 788 | files = [ 789 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 790 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 791 | ] 792 | 793 | [[package]] 794 | name = "sqlparse" 795 | version = "0.4.4" 796 | description = "A non-validating SQL parser." 797 | optional = false 798 | python-versions = ">=3.5" 799 | groups = ["dev"] 800 | files = [ 801 | {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, 802 | {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, 803 | ] 804 | 805 | [package.extras] 806 | dev = ["build", "flake8"] 807 | doc = ["sphinx"] 808 | test = ["pytest", "pytest-cov"] 809 | 810 | [[package]] 811 | name = "tomli" 812 | version = "2.0.1" 813 | description = "A lil' TOML parser" 814 | optional = false 815 | python-versions = ">=3.7" 816 | groups = ["dev"] 817 | markers = "python_version < \"3.11\"" 818 | files = [ 819 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 820 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 821 | ] 822 | 823 | [[package]] 824 | name = "tomlkit" 825 | version = "0.12.3" 826 | description = "Style preserving TOML library" 827 | optional = false 828 | python-versions = ">=3.7" 829 | groups = ["dev"] 830 | files = [ 831 | {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, 832 | {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, 833 | ] 834 | 835 | [[package]] 836 | name = "typing-extensions" 837 | version = "4.9.0" 838 | description = "Backported and Experimental Type Hints for Python 3.8+" 839 | optional = false 840 | python-versions = ">=3.8" 841 | groups = ["dev"] 842 | files = [ 843 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 844 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 845 | ] 846 | 847 | [[package]] 848 | name = "tzdata" 849 | version = "2023.4" 850 | description = "Provider of IANA time zone data" 851 | optional = false 852 | python-versions = ">=2" 853 | groups = ["dev"] 854 | markers = "sys_platform == \"win32\"" 855 | files = [ 856 | {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, 857 | {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, 858 | ] 859 | 860 | [[package]] 861 | name = "virtualenv" 862 | version = "20.25.0" 863 | description = "Virtual Python Environment builder" 864 | optional = false 865 | python-versions = ">=3.7" 866 | groups = ["dev"] 867 | files = [ 868 | {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, 869 | {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, 870 | ] 871 | 872 | [package.dependencies] 873 | distlib = ">=0.3.7,<1" 874 | filelock = ">=3.12.2,<4" 875 | platformdirs = ">=3.9.1,<5" 876 | 877 | [package.extras] 878 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 879 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 880 | 881 | [[package]] 882 | name = "zipp" 883 | version = "3.17.0" 884 | description = "Backport of pathlib-compatible object wrapper for zip files" 885 | optional = false 886 | python-versions = ">=3.8" 887 | groups = ["dev"] 888 | markers = "python_version < \"3.10\"" 889 | files = [ 890 | {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, 891 | {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, 892 | ] 893 | 894 | [package.extras] 895 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] 896 | testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] 897 | 898 | [metadata] 899 | lock-version = "2.1" 900 | python-versions = ">=3.9" 901 | content-hash = "661cbe30458eb8dfc6137f5df9361dfbd7c524e2b599bb7c343d7ed62eacfb10" 902 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [project] 6 | name = "jsonstar" 7 | 8 | [project.urls] 9 | Homepage = "https://github.com/henriquebastos/python-jsonstar" 10 | Documentation = "https://github.com/henriquebastos/python-jsonstar" 11 | Repository = "https://github.com/henriquebastos/python-jsonstar" 12 | Issues = "https://github.com/henriquebastos/python-jsonstar/issues" 13 | 14 | [tool.black] 15 | line-length = 120 16 | target-version = ['py311'] 17 | exclude = ''' 18 | /( 19 | \.git 20 | | \.venv 21 | | \.idea 22 | | build 23 | | dist 24 | )/ 25 | ''' 26 | 27 | [tool.isort] 28 | atomic = true 29 | case_sensitive = true 30 | filter_files = true 31 | line_length = 120 32 | lines_after_imports = 2 33 | profile = "black" 34 | py_version = "auto" 35 | remove_redundant_aliases = true 36 | 37 | [tool.poetry] 38 | name = "jsonstar" 39 | version = "1.1.1" 40 | description = "Extensible JSON module to serialize all objects." 41 | authors = ["Henrique Bastos "] 42 | license = "MIT" 43 | readme = "README.md" 44 | packages = [ 45 | {include = "jsonstar", from = "."}, 46 | ] 47 | classifiers = [ 48 | "Development Status :: 5 - Production/Stable", 49 | "Intended Audience :: Developers", 50 | "License :: OSI Approved :: MIT License", 51 | "Programming Language :: Python :: 3", 52 | "Programming Language :: Python :: 3.8", 53 | "Programming Language :: Python :: 3.9", 54 | "Programming Language :: Python :: 3.10", 55 | "Programming Language :: Python :: 3.11", 56 | "Programming Language :: Python :: Implementation :: CPython", 57 | "Programming Language :: Python :: Implementation :: PyPy", 58 | "Operating System :: OS Independent", 59 | "Topic :: Software Development :: Libraries :: Python Modules", 60 | ] 61 | 62 | [tool.poetry.dependencies] 63 | python = ">=3.9" 64 | 65 | [tool.poetry.group.dev.dependencies] 66 | black = "^24.1.1" 67 | flake8 = "^7.0.0" 68 | freezegun = "^1.4.0" 69 | isort = "^5.13.2" 70 | pre-commit = "^3.6.0" 71 | pylint = "^3.0.3" 72 | pytest = "^8.0.0" 73 | pytz = "^2024.1" 74 | pydantic = "^2.6.0" 75 | attrs = "^23.2.0" 76 | django = "^4.2.10" 77 | build = "^1.0.3" 78 | 79 | [tool.pytest.ini_options] 80 | python_files = 'test*.py' 81 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henriquebastos/python-jsonstar/6fb683e8ca6aa53ce24d0c7de36b1b0bc7b62f75/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_default_encoder.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import uuid 3 | from datetime import date, datetime, time, timedelta 4 | 5 | import attrs 6 | import pytest 7 | from freezegun import freeze_time 8 | from pydantic import BaseModel 9 | from pytz import timezone 10 | 11 | from jsonstar import JSONEncoderStar 12 | 13 | 14 | def encode(o): 15 | return JSONEncoderStar().encode(o).strip("'\"") 16 | 17 | 18 | class TestDatetimeEncoder: 19 | @pytest.mark.parametrize( 20 | "tz,expected", 21 | [ 22 | ("UTC", "2023-01-01T13:45:30.643Z"), 23 | ("US/Eastern", "2023-01-01T13:45:30.643-04:56"), 24 | ("America/Sao_Paulo", "2023-01-01T13:45:30.643-03:06"), 25 | ("Australia/Sydney", "2023-01-01T13:45:30.643+10:05"), 26 | ], 27 | ) 28 | def test_encode_datetime_as_ecma262_different_timezones(self, tz, expected): 29 | dt = datetime(2023, 1, 1, 13, 45, 30, 643768, tzinfo=timezone(tz)) 30 | assert encode(dt) == expected 31 | 32 | 33 | class TestDateEncoder: 34 | def test_encode_date_as_iso_string(self): 35 | assert encode(date(2022, 1, 1)) == "2022-01-01" 36 | 37 | def test_encode_date_as_iso_string_edge_case(self): 38 | assert encode(date(1, 1, 1)) == "0001-01-01" 39 | 40 | 41 | class TestTimeEncoder: 42 | def time_encoder_handles_valid_time(self): 43 | assert encode(time(13, 45, 30, 123456)) == "13:45:30.123" 44 | 45 | def time_encoder_handles_edge_case_no_microseconds(self): 46 | assert encode(time(0, 0, 0)) == "00:00:00.000" 47 | 48 | def time_encoder_handles_edge_case_max_time(self): 49 | assert encode(time(23, 59, 59, 999999)) == "23:59:59.999" 50 | 51 | 52 | class TestTimedeltaEncoder: 53 | def test_encode_timedelta_as_iso_string_positive_duration(self): 54 | duration = timedelta(days=2, hours=3, minutes=4, seconds=5) 55 | assert encode(duration) == "P2DT03H04M05S" 56 | 57 | def test_encode_timedelta_as_iso_string_negative_duration(self): 58 | duration = timedelta(days=-2, hours=-3, minutes=-4, seconds=-5) 59 | assert encode(duration) == "-P2DT03H04M05S" 60 | 61 | def test_encode_timedelta_as_iso_string_zero_duration(self): 62 | duration = timedelta(0) 63 | assert encode(duration) == "P0DT00H00M00S" 64 | 65 | def test_encode_timedelta_as_iso_string_with_microseconds(self): 66 | duration = timedelta(seconds=1, microseconds=123456) 67 | assert encode(duration) == "P0DT00H00M01.123456S" 68 | 69 | 70 | class TestDecimalEncoder: 71 | def test_decimal_encoder_handles_valid_decimal(self): 72 | from decimal import Decimal 73 | 74 | assert encode(Decimal("10.5")) == "10.5" 75 | 76 | def test_decimal_encoder_handles_zero(self): 77 | from decimal import Decimal 78 | 79 | assert encode(Decimal("0")) == "0" 80 | 81 | def test_decimal_encoder_handles_negative_decimal(self): 82 | from decimal import Decimal 83 | 84 | assert encode(Decimal("-10.5")) == "-10.5" 85 | 86 | 87 | class TestUUIDEncoder: 88 | def test_uuid_encoder_handles_valid_uuid(self): 89 | u = uuid.uuid4() 90 | assert encode(u) == str(u) 91 | 92 | 93 | class TestSetEncoder: 94 | def test_set_encoder_handles_valid_set(self): 95 | assert encode({1, 2, 3}) == "[1, 2, 3]" 96 | 97 | def test_set_encoder_handles_empty_set(self): 98 | assert encode(set()) == "[]" 99 | 100 | def test_set_encoder_handles_set_with_multiple_same_elements(self): 101 | assert encode({1, 1, 2, 2, 3, 3}) == "[1, 2, 3]" 102 | 103 | 104 | class TestFrozensetEncoder: 105 | def test_frozenset_encoder_handles_valid_frozenset(self): 106 | assert encode(frozenset({1, 2, 3})) == "[1, 2, 3]" 107 | 108 | def test_frozenset_encoder_handles_empty_frozenset(self): 109 | assert encode(frozenset()) == "[]" 110 | 111 | def test_frozenset_encoder_handles_frozenset_with_multiple_same_elements(self): 112 | assert encode(frozenset({1, 1, 2, 2, 3, 3})) == "[1, 2, 3]" 113 | 114 | 115 | @freeze_time("2024-01-01") 116 | class TestDjangoModelEncoder: 117 | @pytest.fixture(autouse=True, scope="class") 118 | def user(self): 119 | import django 120 | from django.conf import settings 121 | 122 | settings.configure( 123 | INSTALLED_APPS=[ 124 | "django.contrib.auth", 125 | "django.contrib.contenttypes", 126 | ] 127 | ) 128 | django.setup() 129 | 130 | from django.contrib.auth.models import User 131 | 132 | return User 133 | 134 | def test_django_model_encoder_handles_valid_model(self, user): 135 | assert encode(user(username="testuser", password="testpass")) == ( 136 | '{"id": null, "password": "testpass", "last_login": null, "is_superuser": false, "username": "testuser", ' 137 | '"first_name": "", "last_name": "", "email": "", "is_staff": false, "is_active": true, "date_joined": ' 138 | '"2024-01-01T00:00:00.000", "groups": [], "user_permissions": []}' 139 | ) 140 | 141 | def test_django_model_encoder_handles_empty_model(self, user): 142 | assert encode(user()) == ( 143 | '{"id": null, "password": "", "last_login": null, "is_superuser": false, "username": "", "first_name": "", ' 144 | '"last_name": "", "email": "", "is_staff": false, "is_active": true, ' 145 | '"date_joined": "2024-01-01T00:00:00.000", "groups": [], "user_permissions": []}' 146 | ) 147 | 148 | def test_django_model_encoder_handles_model_with_null_fields(self, user): 149 | assert encode(user(username=None, password=None)) == ( 150 | '{"id": null, "password": null, "last_login": null, "is_superuser": false, "username": null, ' 151 | '"first_name": "", "last_name": "", "email": "", "is_staff": false, "is_active": true, ' 152 | '"date_joined": "2024-01-01T00:00:00.000", "groups": [], "user_permissions": []}' 153 | ) 154 | 155 | 156 | class TestPydanticEncoder: 157 | def test_pydantic_encoder_handles_valid_model(self): 158 | class PydanticModel(BaseModel): 159 | id: int 160 | name: str 161 | 162 | assert encode(PydanticModel(id=1, name="test")) == '{"id": 1, "name": "test"}' 163 | 164 | def test_pydantic_encoder_handles_empty_model(self): 165 | from pydantic import BaseModel 166 | 167 | class PydanticModel(BaseModel): 168 | id: int = None 169 | name: str = None 170 | 171 | assert encode(PydanticModel()) == '{"id": null, "name": null}' 172 | 173 | 174 | class TestAttrsFunctionalEncoders: 175 | def test_attrs_functional_encoders_are_not_empty_when_attrs_imported(self): 176 | @attrs.define 177 | class AttrsClass: 178 | x: int 179 | 180 | assert encode(AttrsClass(x=5)) == '{"x": 5}' 181 | 182 | 183 | class TestDataclassFunctionalEncoders: 184 | def test_dataclass_functional_encoder(self): 185 | @dataclasses.dataclass 186 | class DataclassClass: 187 | x: int 188 | 189 | assert encode(DataclassClass(x=5)) == '{"x": 5}' 190 | -------------------------------------------------------------------------------- /tests/test_encoder.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | from jsonstar.encoder import JSONEncoderStar 6 | 7 | 8 | class CustomType: 9 | """ 10 | CustomType to test our implementation of the default method. 11 | Types supported by the standard JSONEncoder are encoded before our code run. 12 | """ 13 | 14 | 15 | class JSONEncoderTest(JSONEncoderStar): 16 | pass 17 | 18 | 19 | @pytest.fixture 20 | def encoder(): 21 | return JSONEncoderTest() 22 | 23 | 24 | @pytest.fixture(autouse=True) 25 | def empty_encoder_class(monkeypatch): 26 | monkeypatch.setattr(JSONEncoderTest, "_default_functional_encoders", []) 27 | monkeypatch.setattr(JSONEncoderTest, "_default_typed_encoders", {}) 28 | 29 | 30 | class TestTypedEncoders: 31 | def test_default_typed_encoders_are_used_when_nothing_else_is_registered(self, encoder): 32 | encoder.__class__._default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"} 33 | 34 | assert encoder.encode(CustomType()) == '"CustomType default encoder"' 35 | 36 | def test_typed_encoders_have_precedence_over_default_type_encoders(self, encoder): 37 | encoder.__class__._default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"} 38 | encoder.register(lambda o: "CustomType encoder", CustomType) 39 | 40 | assert encoder.encode(CustomType()) == '"CustomType encoder"' 41 | 42 | def test_typed_encoders_have_precedence_over_functional_encoders(self, encoder): 43 | encoder.__class__._default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"} 44 | encoder.register(lambda o: "CustomType encoder", CustomType) 45 | encoder.register(lambda o: "Functional encoder") 46 | 47 | assert encoder.encode(CustomType()) == '"CustomType encoder"' 48 | 49 | def test_absence_of_typed_encoder_leads_to_functional_encoder_being_used(self, encoder): 50 | encoder.register(lambda o: "Functional encoder") 51 | 52 | assert encoder.encode(CustomType()) == '"Functional encoder"' 53 | 54 | def test_inherited_types_are_supported_by_base_type_encoder(self, encoder): 55 | class InheritedType(CustomType): 56 | pass 57 | 58 | encoder.register(lambda o: "CustomType encoder", CustomType) 59 | 60 | assert encoder.encode(InheritedType()) == '"CustomType encoder"' 61 | 62 | def test_encoder_for_inherited_type_has_precedence_over_encoder_for_base_type(self, encoder): 63 | class Mother(CustomType): 64 | pass 65 | 66 | class Father(CustomType): 67 | pass 68 | 69 | class Child(Mother, Father): 70 | pass 71 | 72 | encoder.register(lambda o: "CustomType encoder", CustomType) 73 | encoder.register(lambda o: "Father encoder", Father) 74 | encoder.register(lambda o: "Child encoder", Child) 75 | 76 | assert encoder.encode(CustomType()) == '"CustomType encoder"' 77 | assert encoder.encode(Mother()) == '"CustomType encoder"' 78 | assert encoder.encode(Father()) == '"Father encoder"' 79 | assert encoder.encode(Child()) == '"Child encoder"' 80 | 81 | 82 | class TestFunctionalEncoders: 83 | def test_default_functional_encoders_are_used_when_nothing_else_is_registered(self, encoder): 84 | encoder.__class__._default_functional_encoders = [lambda o: "default functional encoder"] 85 | 86 | assert encoder.encode(CustomType()) == '"default functional encoder"' 87 | 88 | def test_functional_encoders_have_precedence_over_default_functional_encoders(self, encoder): 89 | encoder.__class__._default_functional_encoders = [lambda o: "default functional encoder"] 90 | encoder.register(lambda o: "functional encoder") 91 | 92 | assert encoder.encode(CustomType()) == '"functional encoder"' 93 | 94 | def test_absence_of_encoders_leads_to_super_error(self, encoder): 95 | with pytest.raises(TypeError): 96 | encoder.encode(CustomType()) 97 | 98 | 99 | class TestRegistrationOfEncoders: 100 | def test_registration_of_typed_encoder(self, encoder): 101 | encoder.register(lambda o: "CustomType encoder", CustomType) 102 | encoder.register(failing_functional_encoder := Mock(side_effect=Exception)) 103 | 104 | assert encoder.encode(CustomType()) == '"CustomType encoder"' 105 | assert not failing_functional_encoder.called 106 | 107 | def test_registration_of_functional_encoder(self, encoder): 108 | encoder.register(failing_functional_encoder := Mock(side_effect=Exception)) 109 | encoder.register(lambda o: "Functional encoder") 110 | 111 | assert encoder.encode(CustomType()) == '"Functional encoder"' 112 | assert failing_functional_encoder.called 113 | 114 | def test_override_typed_encoder(self, encoder): 115 | encoder.register(lambda o: "CustomType encoder", CustomType) 116 | encoder.register(lambda o: "CustomType encoder override", CustomType) 117 | 118 | assert encoder.encode(CustomType()) == '"CustomType encoder override"' 119 | 120 | 121 | class TestRegistrationOnInit: 122 | def test_registration_of_typed_encoder_on_init(self): 123 | encoder = JSONEncoderTest(typed_encoders={CustomType: lambda o: "CustomType encoder"}) 124 | 125 | assert encoder.encode(CustomType()) == '"CustomType encoder"' 126 | 127 | def test_registration_of_functional_encoder_on_init(self): 128 | encoder = JSONEncoderTest(functional_encoders=[lambda o: "Functional encoder"]) 129 | 130 | assert encoder.encode(CustomType()) == '"Functional encoder"' 131 | 132 | def test_override_typed_encoder_on_init(self): 133 | encoder = JSONEncoderTest(typed_encoders={CustomType: lambda o: "CustomType encoder"}) 134 | encoder.register(lambda o: "CustomType encoder override", CustomType) 135 | 136 | assert encoder.encode(CustomType()) == '"CustomType encoder override"' 137 | -------------------------------------------------------------------------------- /tests/test_register_defaults.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import jsonstar 4 | from jsonstar import JSONEncoderStar 5 | 6 | 7 | class CustomType: 8 | """ 9 | CustomType to test our implementation of the default method. 10 | Types supported by the standard JSONEncoder are encoded before our code run. 11 | """ 12 | 13 | 14 | class TestDefaultEncoderRegistration: 15 | @pytest.fixture 16 | def empty_encoder(self, monkeypatch): 17 | monkeypatch.setattr(jsonstar.JSONEncoderStar, "_default_functional_encoders", []) 18 | monkeypatch.setattr(jsonstar.JSONEncoderStar, "_default_typed_encoders", {}) 19 | 20 | def test_register_default_encoder_with_module_api(self, empty_encoder): 21 | jsonstar.register_default_encoder(lambda o: "functional default encoder") 22 | jsonstar.register_default_encoder(lambda o: "typed default encoder", CustomType) 23 | 24 | assert jsonstar.dumps(CustomType()) == '"typed default encoder"' 25 | assert jsonstar.dumps(object()) == '"functional default encoder"' 26 | 27 | def test_register_default_encoder_with_classmethod(self, empty_encoder): 28 | JSONEncoderStar.register_default_encoder(lambda o: "functional default encoder") 29 | JSONEncoderStar.register_default_encoder(lambda o: "typed default encoder", CustomType) 30 | 31 | assert jsonstar.dumps(CustomType()) == '"typed default encoder"' 32 | assert jsonstar.dumps(object()) == '"functional default encoder"' 33 | 34 | 35 | class TestIsolateDefaultsFromEncoderClasses: 36 | def test_isolate_functional_defaults_from_different_encoder_classes(self): 37 | class EncoderA1(JSONEncoderStar): 38 | pass 39 | 40 | class EncoderA2(EncoderA1): 41 | pass 42 | 43 | class EncoderB1(JSONEncoderStar): 44 | pass 45 | 46 | def a1(o): 47 | return o 48 | 49 | def a2(o): 50 | return o 51 | 52 | def b1(o): 53 | return o 54 | 55 | default_functional_encoders = JSONEncoderStar._default_functional_encoders.copy() 56 | 57 | EncoderA1.register_default_encoder(a1) 58 | EncoderA2.register_default_encoder(a2) 59 | EncoderB1.register_default_encoder(b1) 60 | 61 | assert JSONEncoderStar.default_functional_encoders() == default_functional_encoders 62 | assert EncoderA1.default_functional_encoders() == [a1] + default_functional_encoders 63 | assert EncoderA2.default_functional_encoders() == [a2, a1] + default_functional_encoders 64 | assert EncoderB1.default_functional_encoders() == [b1] + default_functional_encoders 65 | 66 | def test_isolate_typed_defaults_from_different_encoder_classes(self): 67 | class EncoderA1(JSONEncoderStar): 68 | pass 69 | 70 | class EncoderA2(EncoderA1): 71 | pass 72 | 73 | class EncoderB1(JSONEncoderStar): 74 | pass 75 | 76 | def a1(o): 77 | return o 78 | 79 | def a2(o): 80 | return o 81 | 82 | def b1(o): 83 | return o 84 | 85 | default_typed_encoders = JSONEncoderStar._default_typed_encoders.copy() 86 | 87 | EncoderA1.register_default_encoder(a1, str) 88 | EncoderA1.register_default_encoder(a1, int) 89 | EncoderA2.register_default_encoder(a2, str) 90 | EncoderB1.register_default_encoder(b1, str) 91 | 92 | assert JSONEncoderStar.default_typed_encoders() == default_typed_encoders 93 | assert EncoderA1.default_typed_encoders() == {**default_typed_encoders, str: a1, int: a1} 94 | assert EncoderA2.default_typed_encoders() == {**default_typed_encoders, str: a2, int: a1} 95 | assert EncoderB1.default_typed_encoders() == {**default_typed_encoders, str: b1} 96 | --------------------------------------------------------------------------------