pax_global_header00006660000000000000000000000064147521302320014511gustar00rootroot0000000000000052 comment=bb035d25f6efabb959c1250af10d49c8c19d0ff1 serializable-2.0.0/000077500000000000000000000000001475213023200141565ustar00rootroot00000000000000serializable-2.0.0/.editorconfig000066400000000000000000000011021475213023200166250ustar00rootroot00000000000000# EditorConfig is awesome: https://EditorConfig.org root = true [*] insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true end_of_line = lf [*.py] indent_style = space indent_size = 4 [*.{yml,yaml}] indent_style = space indent_size = 2 [*.toml] indent_style = space indent_size = 2 [*.md] charset = latin1 indent_style = space indent_size = 2 # 2 trailing spaces indicate line breaks. trim_trailing_whitespace = false [*.{rst,txt}] indent_style = space indent_size = 4 [{*.ini,.bandit,.flake8}] charset = latin1 indent_style = space indent_size = 4 serializable-2.0.0/.flake8000066400000000000000000000010531475213023200153300ustar00rootroot00000000000000[flake8] ## https://flake8.pycqa.org/en/latest/user/configuration.html ## keep in sync with isort config - in `isort.cfg` file exclude = build,dist,__pycache__,.eggs,*.egg-info*, *_cache,*.cache, .git,.tox,.venv,venv _OLD,_TEST, docs max-line-length = 120 # max-complexity = 10 ignore = # ignore `self`, `cls` markers of flake8-annotations>=2.0 ANN101,ANN102 # ignore Opinionated Warnings - which are documented as disabled by default # See https://github.com/sco1/flake8-annotations#opinionated-warnings ANN401serializable-2.0.0/.github/000077500000000000000000000000001475213023200155165ustar00rootroot00000000000000serializable-2.0.0/.github/dependabot.yml000066400000000000000000000014321475213023200203460ustar00rootroot00000000000000# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: 'github-actions' directory: '/' schedule: interval: 'weekly' day: 'saturday' labels: [ 'dependencies' ] commit-message: prefix: 'chore' ## prefix maximum string length of 15 include: 'scope' open-pull-requests-limit: 999 - package-ecosystem: 'pip' directory: '/' schedule: interval: 'weekly' day: 'saturday' allow: - dependency-type: 'all' versioning-strategy: 'auto' labels: [ 'dependencies' ] commit-message: prefix: 'chore' ## prefix maximum string length of 15 include: 'scope' open-pull-requests-limit: 999 serializable-2.0.0/.github/workflows/000077500000000000000000000000001475213023200175535ustar00rootroot00000000000000serializable-2.0.0/.github/workflows/python.yml000066400000000000000000000121401475213023200216150ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. name: Python CI on: workflow_dispatch: pull_request: branches-ignore: ['dependabot/**'] push: tags: [ 'v*.*.*' ] # run again on release tags to have tools mark them branches: [ 'main' ] schedule: # schedule weekly tests, since some dependencies are not intended to be pinned # this means: at 23:42 on Fridays - cron: '42 23 * * 5' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: REPORTS_DIR: CI_reports PYTHON_VERSION_DEFAULT: "3.11" POETRY_VERSION: "1.8.1" jobs: coding-standards: name: Linting & Coding Standards runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout # see https://github.com/actions/checkout uses: actions/checkout@v4 - name: Setup Python Environment # see https://github.com/actions/setup-python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION_DEFAULT }} architecture: 'x64' - name: Install poetry # see https://github.com/marketplace/actions/setup-poetry uses: Gr1N/setup-poetry@v9 with: poetry-version: ${{ env.POETRY_VERSION }} - name: Install dependencies run: poetry install - name: Run tox run: poetry run tox run -e flake8 -s false static-code-analysis: name: Static Coding Analysis (py${{ matrix.python-version}} ${{ matrix.toxenv-factor }}) runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: include: - # latest python python-version: '3.12' toxenv-factor: 'current' - # lowest supported python python-version: '3.8' toxenv-factor: 'lowest' steps: - name: Checkout # see https://github.com/actions/checkout uses: actions/checkout@v4 - name: Setup Python Environment # see https://github.com/actions/setup-python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} architecture: 'x64' - name: Install poetry # see https://github.com/marketplace/actions/setup-poetry uses: Gr1N/setup-poetry@v9 with: poetry-version: ${{ env.POETRY_VERSION }} - name: Install Dependencies run: poetry install --no-interaction --no-root - name: Run tox run: poetry run tox run -r -e mypy-${{ matrix.toxenv-factor }} -s false build-and-test: name: Test (${{ matrix.os }} py${{ matrix.python-version }}) runs-on: ${{ matrix.os }} timeout-minutes: 10 env: REPORTS_ARTIFACT: tests-reports strategy: fail-fast: false matrix: os: - ubuntu-latest - windows-latest - macos-13 # macos-latest has issues with py38 python-version: - "3.12" # highest supported - "3.11" - "3.10" - "3.9" - "3.8" # lowest supported steps: - name: Checkout # see https://github.com/actions/checkout uses: actions/checkout@v4 - name: Create reports directory run: mkdir ${{ env.REPORTS_DIR }} - name: Setup Python Environment # see https://github.com/actions/setup-python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} architecture: 'x64' - name: Install poetry # see https://github.com/marketplace/actions/setup-poetry uses: Gr1N/setup-poetry@v9 with: poetry-version: ${{ env.POETRY_VERSION }} - name: Install dependencies run: poetry install - name: Ensure build successful run: poetry build - name: Run tox run: poetry run tox run -v -r -e py -s false - name: Generate coverage reports run: > poetry run coverage report && poetry run coverage xml -o ${{ env.REPORTS_DIR }}/coverage-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.toxenv-factor }}.xml && poetry run coverage html -d ${{ env.REPORTS_DIR }} - name: Artifact reports if: ${{ ! cancelled() }} # see https://github.com/actions/upload-artifact uses: actions/upload-artifact@v4 with: name: ${{ env.REPORTS_ARTIFACT }}_${{ matrix.os }}_py${{ matrix.python-version }} path: ${{ env.REPORTS_DIR }} if-no-files-found: error serializable-2.0.0/.github/workflows/release.yml000066400000000000000000000123621475213023200217220ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. name: Release on: push: branches: [ 'main', 'master' ] workflow_dispatch: inputs: release_force: # see https://python-semantic-release.readthedocs.io/en/latest/github-action.html#command-line-options description: | Force release be one of: [major | minor | patch] Leave empty for auto-detect based on commit messages. type: choice options: - "" # auto - no force - major # force major - minor # force minor - patch # force patch default: "" required: false prerelease_token: description: 'The "prerelease identifier" to use as a prefix for the "prerelease" part of a semver. Like the rc in `1.2.0-rc.8`.' type: choice options: - rc - beta - alpha default: rc required: false prerelease: description: "Is a pre-release" type: boolean default: false required: false concurrency: group: deploy cancel-in-progress: false # prevent hickups with semantic-release env: PYTHON_VERSION_DEFAULT: "3.11" POETRY_VERSION: "1.8.1" jobs: quicktest: runs-on: ubuntu-latest steps: - name: Checkout code # see https://github.com/actions/checkout uses: actions/checkout@v4 - name: Setup Python Environment # see https://github.com/actions/setup-python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION_DEFAULT }} architecture: 'x64' - name: Install poetry # see https://github.com/marketplace/actions/setup-poetry uses: Gr1N/setup-poetry@v9 with: poetry-version: ${{ env.POETRY_VERSION }} - name: Install dependencies run: poetry install --no-root - name: Run tox run: poetry run tox run -e py -s false release: needs: - quicktest # https://github.community/t/how-do-i-specify-job-dependency-running-in-another-workflow/16482 # limit this to being run on regular commits, not the commits that semantic-release will create # but also allow manual workflow dispatch if: "!contains(github.event.head_commit.message, 'chore(release):')" runs-on: ubuntu-latest permissions: # NOTE: this enables trusted publishing. # See https://github.com/pypa/gh-action-pypi-publish/tree/release/v1#trusted-publishing # and https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ id-token: write contents: write steps: - name: Checkout # see https://github.com/actions/checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python Environment # see https://github.com/actions/setup-python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION_DEFAULT }} architecture: 'x64' - name: Install and configure Poetry # See https://github.com/marketplace/actions/install-poetry-action uses: snok/install-poetry@v1 with: version: ${{ env.POETRY_VERSION }} virtualenvs-create: true virtualenvs-in-project: true installer-parallel: true - name: Install dependencies run: poetry install --no-root - name: View poetry version run: poetry --version - name: Python Semantic Release id: release # see https://python-semantic-release.readthedocs.io/en/latest/automatic-releases/github-actions.html # see https://github.com/python-semantic-release/python-semantic-release uses: python-semantic-release/python-semantic-release@v8.5.1 with: github_token: ${{ secrets.GITHUB_TOKEN }} force: ${{ github.event.inputs.release_force }} prerelease: ${{ github.event.inputs.prerelease }} prerelease_token: ${{ github.event.inputs.prerelease_token }} - name: Publish package distributions to PyPI if: steps.release.outputs.released == 'true' # see https://github.com/pypa/gh-action-pypi-publish uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_TOKEN }} - name: Publish package distributions to GitHub Releases if: steps.release.outputs.released == 'true' # see https://github.com/python-semantic-release/upload-to-gh-release uses: python-semantic-release/upload-to-gh-release@main with: github_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ steps.release.outputs.tag }} serializable-2.0.0/.gitignore000066400000000000000000000063021475213023200161470ustar00rootroot00000000000000 # Exclude coverage test-reports /.pytype/ # Exlude IDE related files .idea/* .vscode/* # # # # # copy from https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore # # # # # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ serializable-2.0.0/.isort.cfg000066400000000000000000000022711475213023200160570ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. [settings] ## read the docs: https://pycqa.github.io/isort/docs/configuration/options.html ## keep in sync with flake8 config - in `tox.ini` file known_first_party = py_serializable skip_gitignore = true skip_glob = build/*,dist/*,__pycache__,.eggs,*.egg-info*, *_cache,*.cache, .git/*,.tox/*,.venv/*,venv/* _OLD/*,_TEST/*, docs/* combine_as_imports = true default_section = THIRDPARTY ensure_newline_before_comments = true include_trailing_comma = true line_length = 120 multi_line_output = 3 serializable-2.0.0/.mypy.ini000066400000000000000000000027411475213023200157370ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. [mypy] files = py_serializable/, tests/model.py show_error_codes = True pretty = True warn_unreachable = True allow_redefinition = False # ignore_missing_imports = False # follow_imports = normal # follow_imports_for_stubs = True ### Strict mode ### warn_unused_configs = True disallow_subclassing_any = True disallow_any_generics = True disallow_untyped_calls = True disallow_untyped_defs = True disallow_incomplete_defs = True check_untyped_defs = True disallow_untyped_decorators = True no_implicit_optional = True warn_redundant_casts = True warn_unused_ignores = False warn_return_any = True no_implicit_reexport = True [mypy-pytest.*] ignore_missing_imports = True [mypy-tests.*] disallow_untyped_decorators = False serializable-2.0.0/.pytype.toml000066400000000000000000000051641475213023200164710ustar00rootroot00000000000000# config file for https://pypi.org/project/pytype/ # generated via `pytype --generate-config .pytype.toml` # run via `pytype --config .pytype.toml` # NOTE: All relative paths are relative to the location of this file. [tool.pytype] # Space-separated list of files or directories to exclude. exclude = [ '**/*_test.py', '**/test_*.py', ] # Space-separated list of files or directories to process. inputs = [ 'py_serializable', # 'tests/model.py' ] # Keep going past errors to analyze as many files as possible. keep_going = true # Run N jobs in parallel. When 'auto' is used, this will be equivalent to the # number of CPUs on the host system. # jobs = 4 # All pytype output goes here. output = '.pytype' # Platform (e.g., "linux", "win32") that the target code runs on. # platform = 'linux' # Paths to source code directories, separated by ':'. pythonpath = '.' # Python version (major.minor) of the target code. # python_version = '3.8' # Enable parameter count checks for overriding methods. This flag is temporary # and will be removed once this behavior is enabled by default. overriding_parameter_count_checks = true # Enable parameter count checks for overriding methods with renamed arguments. # This flag is temporary and will be removed once this behavior is enabled by # default. overriding_renamed_parameter_count_checks = false # Use the enum overlay for more precise enum checking. This flag is temporary # and will be removed once this behavior is enabled by default. use_enum_overlay = false # Variables initialized as None retain their None binding. This flag is # temporary and will be removed once this behavior is enabled by default. strict_none_binding = false # Support the third-party fiddle library. This flag is temporary and will be # removed once this behavior is enabled by default. use_fiddle_overlay = false # Opt-in: Do not allow Any as a return type. no_return_any = false # Experimental: Infer precise return types even for invalid function calls. precise_return = false # Experimental: Solve unknown types to label with structural types. protocols = false # Experimental: Only load submodules that are explicitly imported. strict_import = false # Experimental: Enable exhaustive checking of function parameter types. strict_parameter_checks = false # Experimental: Emit errors for comparisons between incompatible primitive # types. strict_primitive_comparisons = false # Experimental: Check that variables are defined in all possible code paths. strict_undefined_checks = false # Space-separated list of error names to ignore. disable = [ 'pyi-error', ] # Don't report errors. report_errors = true serializable-2.0.0/.readthedocs.yaml000066400000000000000000000010331475213023200174020ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Formats formats: all # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . - requirements: docs/requirements.txtserializable-2.0.0/CHANGELOG.md000066400000000000000000001410561475213023200157760ustar00rootroot00000000000000# CHANGELOG ## v2.0.0 (2025-02-09) ### Breaking * refactor!: rename python package `serializable` -> `py_serializable` (#155) The python package was renamed from `serializable` to `py_serializable`. Therefore, you need to adjust your imports. The following shows a quick way to adjust imports in the most efficient way. ### OLD imports ```py import serializable from serializable import ViewType, XmlArraySerializationType, XmlStringSerializationType from serializable.helpers import BaseHelper, Iso8601Date ``` ### ADJUSTED imports ```py import py_serializable as serializable from py_serializable import ViewType, XmlArraySerializationType, XmlStringSerializationType from py_serializable.helpers import BaseHelper, Iso8601Date ``` see migration path: <https://py-serializable.readthedocs.io/en/refactor-rename-installable-py_serializable/migration.html> ---- fixes #151 --------- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> Signed-off-by: semantic-release <semantic-release> Co-authored-by: semantic-release <semantic-release> ([`67afaef`](https://github.com/madpah/serializable/commit/67afaef4a5e670c533409f1ccbc3d2dbb263d2de)) ### Unknown * Delete duplicate CODEOWNERS (#156) we have a codeowners file in root already ([`b64cdde`](https://github.com/madpah/serializable/commit/b64cdde6d4561355f7b92416dfcb36a8ff770be5)) ## v1.1.2 (2024-10-01) ### Fix * fix: date/time deserialization with fractional seconds (#138) fix multiple where fractional seconds were not properly deserialized or deserialization caused unexpected crashes in py<3.11 --------- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> Co-authored-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`f4b1c27`](https://github.com/madpah/serializable/commit/f4b1c27110d1becc76771efffd8dc0a96d629cb5)) ## v1.1.1 (2024-09-16) ### Fix * fix: serializer omit `None` values as expected (#136) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`1a0e14b`](https://github.com/madpah/serializable/commit/1a0e14b8ee0866621a388a09e41c7f173e874e25)) ## v1.1.0 (2024-07-08) ### Documentation * docs: fix links (#122) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`aabb5e9`](https://github.com/madpah/serializable/commit/aabb5e925b5630a02f99dcf07064ddfb65c9064e)) ### Feature * feat: XML string formats for `normalizedString` and `token` (#119) fixes #114 fixes #115 --------- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`3a1728d`](https://github.com/madpah/serializable/commit/3a1728d43a13e57ecad2b3feebadf1d9fdc132c3)) ## v1.0.3 (2024-04-04) ### Fix * fix: support deserialization of XML flat arrays where `child_name` does not conform to current formatter #89 (#90) Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`ade5bd7`](https://github.com/madpah/serializable/commit/ade5bd76cf945b7380dbeac5e6233417da2d26c6)) ## v1.0.2 (2024-03-01) ### Build * build: use poetry v1.8.1 (#81) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`46a8d9e`](https://github.com/madpah/serializable/commit/46a8d9e629ac502864a99acaa9418d1c5cd32388)) ## v1.0.1 (2024-02-13) ### Fix * fix: serialization of `datetime` without timezone with local time offset (#76) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`06776ba`](https://github.com/madpah/serializable/commit/06776baef2cc4b893550320c474128317f6276c1)) ## v1.0.0 (2024-01-22) ### Breaking * feat!: v1.0.0 (#55) **Release of first major version 🎉** ## BREAKING Changes * Dropped support for python <3.8 --------- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`2cee4d5`](https://github.com/madpah/serializable/commit/2cee4d5f48d59a737f4fc7b0e3d26fbce33c2392)) ### Documentation * docs: fix conda link/url Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`5645ca6`](https://github.com/madpah/serializable/commit/5645ca65763198a166c348172cc29147881ad6f2)) ## v0.17.1 (2024-01-07) ### Documentation * docs: add "documentation" url to project meta Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`bf864d7`](https://github.com/madpah/serializable/commit/bf864d75d8a12426d4c71ae9ea1f533e730bd54e)) * docs: add "documentation" url to project meta Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`d3bcc42`](https://github.com/madpah/serializable/commit/d3bcc4258ab8cdf6c9e09b47985997cafdc19e9a)) ### Fix * fix: log placeholder (#60) ([`3cc6cad`](https://github.com/madpah/serializable/commit/3cc6cadad27a86b46ca576540f89a15f0f8fc1cd)) ### Unknown * 0.17.1 chore(release): 0.17.1 Automatically generated by python-semantic-release ([`3b50104`](https://github.com/madpah/serializable/commit/3b501047671da16b6543abc4208d11e61c87b3d9)) * Create SECURITY.md ([`9cdc0b1`](https://github.com/madpah/serializable/commit/9cdc0b1a176b432fd12adbf0379e61a257d3e3ba)) ## v0.17.0 (2024-01-06) ### Documentation * docs: modernixe read-the-docs Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`7ae6aad`](https://github.com/madpah/serializable/commit/7ae6aad3b5939508238d1502c116866ef79949cb)) * docs: homepage (#48) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`de206d6`](https://github.com/madpah/serializable/commit/de206d6083be643a58f08554b61518367f67cda1)) * docs: condaforge (#46) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`c0074ce`](https://github.com/madpah/serializable/commit/c0074ce911f66bc6de0a451b8922f80f1ffa6270)) ### Feature * feat: logger (#47) Reworked the way this library does logging/warning. It utilizes the logger named `serializable` for everything, now. --------- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> Co-authored-by: Kyle Roeschley <kyle.roeschley@ni.com> ([`9269b0e`](https://github.com/madpah/serializable/commit/9269b0e681665abaef3f110925cd098b2438880f)) ### Unknown * 0.17.0 chore(release): 0.17.0 Automatically generated by python-semantic-release ([`a6fc788`](https://github.com/madpah/serializable/commit/a6fc78853e13a3c7e922c7e95ef7cbbaa4bf3b1d)) ## v0.16.0 (2023-11-29) ### Feature * feat: more controll over XML attribute serialization (#34) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`38f42d6`](https://github.com/madpah/serializable/commit/38f42d64e556a85206faa50459a9ce3e889bd3ae)) ### Unknown * 0.16.0 chore(release): 0.16.0 Automatically generated by python-semantic-release ([`b444fd7`](https://github.com/madpah/serializable/commit/b444fd721102caaa51d0854fc6f6408e919a77d5)) ## v0.15.0 (2023-10-10) ### Feature * feat: allow custom (de)normalization (#32) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`aeecd6b`](https://github.com/madpah/serializable/commit/aeecd6b2e8c4e8febc84ebfa24fe7ec96fd9cb10)) ### Unknown * 0.15.0 chore(release): 0.15.0 Automatically generated by python-semantic-release ([`e80c514`](https://github.com/madpah/serializable/commit/e80c5146621e9ed1bfbe2118e36c269aa4cacdb8)) ## v0.14.1 (2023-10-08) ### Fix * fix: JSON deserialize `Decimal` (#31) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`b6dc66a`](https://github.com/madpah/serializable/commit/b6dc66acfb7fdc82b3dd18caf4ad79ec0e87eef0)) ### Unknown * 0.14.1 chore(release): 0.14.1 Automatically generated by python-semantic-release ([`0183a17`](https://github.com/madpah/serializable/commit/0183a174b5b9e402f20e3e240e565b124f2b008b)) ## v0.14.0 (2023-10-06) ### Feature * feat: enhanced typehints and typing (#27) Even tough some structures are refactored, no public API is changed. No runtime is changed. TypeCheckers might behave differently, which is intentional due to bug fixes. This is considered a non-breaking change, as it does not affect runtime. --------- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`410372a`](https://github.com/madpah/serializable/commit/410372a0fa2713c5a36d790f08d2d4b52a6a187c)) ### Unknown * 0.14.0 chore(release): 0.14.0 Automatically generated by python-semantic-release ([`7bb0d1b`](https://github.com/madpah/serializable/commit/7bb0d1b0fcf5b63770c214ec6e784f1f6ba94f58)) ## v0.13.1 (2023-10-06) ### Documentation * docs: add examples to docs (#28) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`4eddb24`](https://github.com/madpah/serializable/commit/4eddb242e51194694474748acdecd38b317b791e)) * docs: remove unnecessary type-ignores Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`26c561d`](https://github.com/madpah/serializable/commit/26c561dc0bf9f5755899a8fa0d0a37aba6275074)) * docs: remove unnecessary type-ignores Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`11b5896`](https://github.com/madpah/serializable/commit/11b5896057fd61838804ea5b52dc3bd0810f6c88)) ### Fix * fix: protect default value for `serialization_types` from unintended downstream modifications (#30) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`0e814f5`](https://github.com/madpah/serializable/commit/0e814f5248176e02a7f96480e54320dde781f8b2)) ### Unknown * 0.13.1 chore(release): 0.13.1 Automatically generated by python-semantic-release ([`bd604c8`](https://github.com/madpah/serializable/commit/bd604c800e1a9ab6101ee8b7b810e92e6288de8b)) ## v0.13.0 (2023-10-01) ### Feature * feat: format specific (de)serialize (#25) Added functionality to implement custom (de)serialization specific for XML or JSON ([#13](https://github.com/madpah/serializable/issues/13)). Changed ------------- * Class `BaseHelper` is no longer abstract. This class does not provide any functionality, it is more like a Protocol with some fallback implementations. * Method `BaseHelper.serialize()` is no longer abstract. Will raise `NotImplementedError` per default. * Method `BaseHelper.deserialize()` is no longer abstract. Will raise `NotImplementedError` per default. Added ---------- * New method `BaseHelper.json_serialize()` predefined. Will call `cls.serialize()` per default. * New method `BaseHelper.json_deserialize()` predefined. Will call `cls.deserialize()` per default. ---- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`dc998df`](https://github.com/madpah/serializable/commit/dc998df37a2ba37fa43d10c8a1ce044a5b9f5b1e)) ### Unknown * 0.13.0 chore(release): 0.13.0 Automatically generated by python-semantic-release ([`c1670d6`](https://github.com/madpah/serializable/commit/c1670d60e7f7adb0fd0f6be2f7cac89fff9315d9)) ## v0.12.1 (2023-10-01) ### Build * build: semantic-release sets library version everywhere (#16) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`296ef19`](https://github.com/madpah/serializable/commit/296ef196e8801b244843814d2d510f1e7d2044d4)) ### Documentation * docs: render only public API (#19) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`fcc5d8e`](https://github.com/madpah/serializable/commit/fcc5d8e6c49e8b8c199cb55f855d09e4259a075a)) * docs: set codeblock language and caption (#15) Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`5d5cf7b`](https://github.com/madpah/serializable/commit/5d5cf7bc29ed70f4024c714b2326012a9db54cea)) ### Fix * fix: xml defaultNamespace serialization and detection (#20) * fixes: serialization with defaultNS fails [#12](https://github.com/madpah/serializable/issues/12) * fixes: defaultNamespace detection fails on XML-attributes when deserializing [#11](https://github.com/madpah/serializable/issues/11) --------- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`59eaa5f`](https://github.com/madpah/serializable/commit/59eaa5f28eb2969e9d497669ef0436eb87b7525d)) ### Unknown * 0.12.1 chore(release): 0.12.1 Automatically generated by python-semantic-release ([`9a2798d`](https://github.com/madpah/serializable/commit/9a2798d23de90ed36a4aecb4ec955cbe037a4089)) * bump to python-semantic-release/python-semantic-release@v7.34.6 Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> ([`68d229e`](https://github.com/madpah/serializable/commit/68d229e62d049713ade8e08487f491683b0bb0f9)) * Merge pull request #7 from claui/fix-top-level-license Keep `LICENSE` in `.dist-info` when building wheel ([`9bc4abc`](https://github.com/madpah/serializable/commit/9bc4abccc9cabed5f9808101a8d25717b86f01b4)) * Keep `LICENSE` in `.dist-info` when building wheel Poetry automatically detects and includes `LICENSE` files in `….dist-info/` when it builds a wheel. If `LICENSE` is also declared as a pattern in Poetry’s `include` list in `pyproject.toml`, then the file will appear in the root directory of the wheel, too: ```plain Path = /var/lib/aurbuild/x86_64/claudia/build/python-py-serializable/src/serializable-0.12.0/dist/py_serializable-0.12.0-py3-none-any.whl Type = zip Physical Size = 22557 Date Time Attr Size Compressed Name ------------------- ----- ------------ ------------ ------------------------ 1980-01-01 00:00:00 ..... 11357 3948 LICENSE 1980-01-01 00:00:00 ..... 52795 9275 serializable/__init__.py 1980-01-01 00:00:00 ..... 3382 923 serializable/formatters.py 1980-01-01 00:00:00 ..... 3690 1180 serializable/helpers.py 1980-01-01 00:00:00 ..... 153 117 serializable/py.typed 1980-01-01 00:00:00 ..... 11357 3948 py_serializable-0.12.0.dist-info/LICENSE 1980-01-01 00:00:00 ..... 3845 1449 py_serializable-0.12.0.dist-info/METADATA 1980-01-01 00:00:00 ..... 88 85 py_serializable-0.12.0.dist-info/WHEEL 2016-01-01 00:00:00 ..... 718 408 py_serializable-0.12.0.dist-info/RECORD ------------------- ----- ------------ ------------ ------------------------ 2016-01-01 00:00:00 87385 21333 9 files ``` Note how the wheel contains two identical copies of your `LICENSE` file: one copy in the `….dist-info/` directory, picked up automatically by Poetry, and a second copy in the root directory of the wheel. Including a generically-named file directly in a wheel’s root directory may cause problems: 1. The `LICENSE` file is going to turn up at the top level of `site-packages` directly. That’s misleading, because anyone who’d browse `site-packages` might conclude that the license be valid for all packages, not just `serializable`, which is incorrect. 2. Having generic files at the top level of `site-packages` causes conflicts with other wheels that happen to include the same file. For example, I’ve had `LICENSE` files coming from two different wheels, excluding `serializable`, sitting at the top level of my `site-packages` directory so I could install only one of them. The fix is to remove the `LICENSE` pattern from the `include` list. Poetry automatically picks up files named `LICENSE`, and drops them either into an sdist’s root directory (when building an sdist) or into `py_serializable-[version].dist-info/` (when building a wheel). Signed-off-by: Claudia <claui@users.noreply.github.com> ([`31e4003`](https://github.com/madpah/serializable/commit/31e4003e949b73a4cd7c18aac458200888c1a0f2)) * Merge branch 'main' of github.com:madpah/serializable ([`c1e8fd8`](https://github.com/madpah/serializable/commit/c1e8fd840b9e89c36f36304342cc6f9be8cc7d26)) ## v0.12.0 (2023-03-07) ### Feature * feat: bump dev dependencies to latest (including `mypy` fixes) Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`06dcaa2`](https://github.com/madpah/serializable/commit/06dcaa28bfebb4505ddc67b287dc6f416822ffb6)) * feat: bump dev dependencies to latest (including `mypy` fixes) Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`6d70287`](https://github.com/madpah/serializable/commit/6d70287640c411d33823e9188b0baa81fba80c24)) ### Unknown * 0.12.0 Automatically generated by python-semantic-release ([`fa9f9b3`](https://github.com/madpah/serializable/commit/fa9f9b39a13120a0b8d47b4fdb9469c2aa642cb6)) * Merge pull request #6 from madpah/fix/dep-updates feat: bump dev dependencies to latest (including `mypy` fixes) ([`08b4825`](https://github.com/madpah/serializable/commit/08b48253bacc62f8a0db54510bf6fe49df68a19f)) ## v0.11.1 (2023-03-03) ### Fix * fix: use `defusedxml` whenever we load XML to prevent XEE attacks ([`ae3d76c`](https://github.com/madpah/serializable/commit/ae3d76c31ab8af81d20acaaba45fd4bb9aad9305)) * fix: use `defusedxml` whenever we load XML to prevent XEE attacks Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`32fd5a6`](https://github.com/madpah/serializable/commit/32fd5a698b41b489b4643bcbe795e24a1e0db423)) * fix: use `defusedxml` whenever we load XML to prevent XEE attacks Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`72e0127`](https://github.com/madpah/serializable/commit/72e01279274246313170e5e7c9d32afec16edf7c)) * fix: use `defusedxml` whenever we load XML to prevent XEE attacks Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`de61deb`](https://github.com/madpah/serializable/commit/de61deb5c2447a656ca6a111194b2b0ceeab9278)) * fix: use `defusedxml` whenever we load XML to prevent XEE attacks Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`de26dc3`](https://github.com/madpah/serializable/commit/de26dc3d0eaab533dac9b1db40f0add56dd67754)) ### Unknown * 0.11.1 Automatically generated by python-semantic-release ([`0bdccc4`](https://github.com/madpah/serializable/commit/0bdccc4a1a4b7fb74f2ea54898e5c08d133f6490)) ## v0.11.0 (2023-03-03) ### Feature * feat: disabled handling to avoid class attributes that clash with `keywords` and `builtins` Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`4439227`](https://github.com/madpah/serializable/commit/44392274628ddec4aaaeae89a8387d435e3cf002)) ### Unknown * 0.11.0 Automatically generated by python-semantic-release ([`90de3b8`](https://github.com/madpah/serializable/commit/90de3b89974aafd39b6b386e0647989c65845e67)) * define `commit_author`? Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`4fad001`](https://github.com/madpah/serializable/commit/4fad001f6c631e23af911bd78469ad1a1ed8d2f6)) * Merge branch 'main' of github.com:madpah/serializable ([`fb46f04`](https://github.com/madpah/serializable/commit/fb46f0438ea81c62adc8bc360bee4b8a24816011)) * enable debug for release Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`2f4d626`](https://github.com/madpah/serializable/commit/2f4d6262a4038b7f3e4da3b0ffe10b6293bd2227)) * Merge pull request #4 from madpah/feat/allow-python-keywords feat: disabled handling to avoid class attributes that clash with `keywords` and `builtins` ([`2a33bc6`](https://github.com/madpah/serializable/commit/2a33bc606e95995ae812e62c9018481c3353962f)) * cleanup Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`0ff402e`](https://github.com/madpah/serializable/commit/0ff402eb99e0073fa03ae0e19b881e352fbca2c7)) ## v0.10.1 (2023-03-02) ### Fix * fix: handle empty XML elements during deserialization Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`f806f35`](https://github.com/madpah/serializable/commit/f806f3521f0afd8978f94f5ec355f47d9a538b91)) ### Unknown * 0.10.1 Automatically generated by python-semantic-release ([`69e5866`](https://github.com/madpah/serializable/commit/69e586630931c088381bfd687a00b83b55d360f8)) ## v0.10.0 (2023-02-21) ### Feature * feat: ability for custom `type_mapping` to take lower priority than `xml_array` Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`fc0bb22`](https://github.com/madpah/serializable/commit/fc0bb22f395498be42394af5f70addb9f63f0b3a)) ### Unknown * 0.10.0 Automatically generated by python-semantic-release ([`58d42ad`](https://github.com/madpah/serializable/commit/58d42ad0455495ad5998694cbd487866d682fed3)) * Merge pull request #3 from madpah/feat/recursive-parsing-differing-schemas feat: `xml_array` has higher priority than `type_mapping` feat: handle `ForwardRef` types ([`664f947`](https://github.com/madpah/serializable/commit/664f947add279dad90ac9cf447a59059ab10d2cc)) * work to handle `ForwardRef` when we have cyclic references in models Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`a66e700`](https://github.com/madpah/serializable/commit/a66e700eeb5a80447522b8112ecdeff0345f0608)) * remove comment Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`6898b40`](https://github.com/madpah/serializable/commit/6898b40b6d55c70ade6e87de4a3cd4b8ce10a028)) * added test to prove https://github.com/CycloneDX/specification/issues/146 for https://github.com/CycloneDX/cyclonedx-python-lib/pull/290 Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`2cfc44d`](https://github.com/madpah/serializable/commit/2cfc44ddc22d3ec5dc860d21297ab76b50102a74)) ## v0.9.3 (2023-01-27) ### Fix * fix: deserializing JSON with custom JSON name was incorrect Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`7d4aefc`](https://github.com/madpah/serializable/commit/7d4aefc98dfe39ae614227601369e9fd25c12faa)) ### Unknown * 0.9.3 Automatically generated by python-semantic-release ([`ccd610f`](https://github.com/madpah/serializable/commit/ccd610f7897e78478da7855095cf02580617340e)) * better logging for deserialization errors Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`a77452d`](https://github.com/madpah/serializable/commit/a77452d38e416aca59ef212379710c044885c383)) * added more logging Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`1f80c4b`](https://github.com/madpah/serializable/commit/1f80c4bb2390cbc5ebef87a8f32cc925f28bbde8)) * code style Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`8ca9e44`](https://github.com/madpah/serializable/commit/8ca9e44c479b35f0e599296b5e462dc87d9bf366)) ## v0.9.2 (2023-01-27) ### Fix * fix: nested array of Enum values in `from_json()` failed Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`ea4d76a`](https://github.com/madpah/serializable/commit/ea4d76a64c8c97f7cb0b16687f300c362dfe7623)) * fix: output better errors when deserializing JSON and we hit errors Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`1699c5b`](https://github.com/madpah/serializable/commit/1699c5b96bb6a8d4f034b29a6fe0521e3d650d53)) ### Unknown * 0.9.2 Automatically generated by python-semantic-release ([`435126c`](https://github.com/madpah/serializable/commit/435126c92032548944fe59243aa5935312ca7bfa)) ## v0.9.1 (2023-01-26) ### Fix * fix: nested array of Enum values in `from_xml()` failed Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`393a425`](https://github.com/madpah/serializable/commit/393a4256abb69228a9e6c2fc76b508e370a39d93)) ### Unknown * 0.9.1 Automatically generated by python-semantic-release ([`f4e018b`](https://github.com/madpah/serializable/commit/f4e018bf109c597ea70ce3a53a9d139aad926d2c)) * doc: added to docs to cover latest features and Views fix: aligned View definition in unit tests with proper practice Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`c7c66f7`](https://github.com/madpah/serializable/commit/c7c66f719b93a9fc2c3929db67d0f7ae0665be7a)) ## v0.9.0 (2023-01-24) ### Feature * feat: bring library to BETA state feat: add support for Python 3.11 Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`c6c36d9`](https://github.com/madpah/serializable/commit/c6c36d911ae401af477bcc98633f10a87140d0a4)) ### Unknown * 0.9.0 Automatically generated by python-semantic-release ([`f5cb856`](https://github.com/madpah/serializable/commit/f5cb85629d6398956a4a1379e44bbd9a1f67d079)) * Merge pull request #2 from madpah/feat/support-py311 feat: bring library to BETA state & add support Python 3.11 ([`33c6756`](https://github.com/madpah/serializable/commit/33c6756d145a15c9d62216acc11568838bf0d1a0)) ## v0.8.2 (2023-01-23) ### Fix * fix: typing for `@serializable.view` was incorrect Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`756032b`](https://github.com/madpah/serializable/commit/756032b543a2fedac1bb61f57796eea438c0f9a7)) * fix: typing for `@serializable.serializable_enum` decorator was incorrect Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`84e7826`](https://github.com/madpah/serializable/commit/84e78262276833f507d4e8a1ce11d4a82733f395)) ### Unknown * 0.8.2 Automatically generated by python-semantic-release ([`3332ed9`](https://github.com/madpah/serializable/commit/3332ed98ae9c9bfae40df743ad4c0ea83eac038b)) * Merge pull request #1 from madpah/fix/typing fix: typing only ([`1860d4d`](https://github.com/madpah/serializable/commit/1860d4df369c8cf9cea917c025bb191fcd242f29)) * spacing Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`fdd5c8a`](https://github.com/madpah/serializable/commit/fdd5c8a344c3ace70170c91272074cbf6d0ebd01)) ## v0.8.1 (2023-01-23) ### Fix * fix: Specific None value per View - support for XML was missing Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`5742861`](https://github.com/madpah/serializable/commit/5742861728d1b371bc0a819fed0b12e9da5829e1)) ### Unknown * 0.8.1 Automatically generated by python-semantic-release ([`c6d9db8`](https://github.com/madpah/serializable/commit/c6d9db8665e8d2c368004d3167d450c5f2f93c28)) ## v0.8.0 (2023-01-20) ### Feature * feat: support for specific None values for Properties by View Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`a80ee35`](https://github.com/madpah/serializable/commit/a80ee3551c5e23f9c0491f48c3f98022317ddd99)) ### Fix * fix: minor typing and styling Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`b728c4c`](https://github.com/madpah/serializable/commit/b728c4c995076cd18317c878c6f5900c6b266425)) * fix: minor typing and styling Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`b2ebcfb`](https://github.com/madpah/serializable/commit/b2ebcfb53cd640eb70a51a9f637db24e0d7b367e)) ### Unknown * 0.8.0 Automatically generated by python-semantic-release ([`4ccdfc9`](https://github.com/madpah/serializable/commit/4ccdfc98b2275efc744de0188152fcdcc560e00f)) ## v0.7.3 (2022-09-22) ### Fix * fix: None value for JSON is now `None` (`null`) fix: typing and coding standards Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`8b7f973`](https://github.com/madpah/serializable/commit/8b7f973cd96c861c4490c50553c880e88ebf33dc)) ### Unknown * 0.7.3 Automatically generated by python-semantic-release ([`8060db3`](https://github.com/madpah/serializable/commit/8060db392f47868bd61bcc333fad51cefd9d2e9f)) * Merge branch 'main' of github.com:madpah/serializable ([`84f957b`](https://github.com/madpah/serializable/commit/84f957b815b2c641218bf7a5d422fa66e787b343)) ## v0.7.2 (2022-09-22) ### Fix * fix: missing namespace for empty XML elements Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`f3659ab`](https://github.com/madpah/serializable/commit/f3659ab9ea651dcd65168aa22fa838d35ee189d5)) ### Unknown * 0.7.2 Automatically generated by python-semantic-release ([`08698d1`](https://github.com/madpah/serializable/commit/08698d10b9b0350458fb079b1ee38e5c118588d7)) ## v0.7.1 (2022-09-15) ### Fix * fix: support forced inclusion of array properties by using `@serializable.include_none` Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`7ad0ecf`](https://github.com/madpah/serializable/commit/7ad0ecf08c5f56de4584f4f081bfc0f667d2f477)) * fix: support for deserializing to objects from a primitive value Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`12f9f97`](https://github.com/madpah/serializable/commit/12f9f9711a5fd924898a0afb50a24c8d360ab3ff)) ### Unknown * 0.7.1 Automatically generated by python-semantic-release ([`01743f2`](https://github.com/madpah/serializable/commit/01743f27db48bb6e896531f1708d11a53571284a)) * Merge branch 'main' of github.com:madpah/serializable ([`eb82dbc`](https://github.com/madpah/serializable/commit/eb82dbc20d558a242620649a6ea8ea8df912283a)) ## v0.7.0 (2022-09-14) ### Feature * feat: support for including `None` values, restricted to certain Views as required fix: tests, imports and formatting Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`614068a`](https://github.com/madpah/serializable/commit/614068a4955f99d8fce5da341a1fd74a6772b775)) ### Unknown * 0.7.0 Automatically generated by python-semantic-release ([`4a007c0`](https://github.com/madpah/serializable/commit/4a007c0b3b2f22c4d26851267390909a01e8adf5)) ## v0.6.0 (2022-09-14) ### Feature * feat: implement views for serialization to JSON and XML Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`db57ef1`](https://github.com/madpah/serializable/commit/db57ef13fa89cc47db074bd9be4b48232842df07)) ### Fix * fix: support for `Decimal` in JSON serialization Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`cc2c20f`](https://github.com/madpah/serializable/commit/cc2c20fe8bce46e4854cb0eecc6702459cd2f99a)) * fix: better serialization to JSON Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`e8b37f2`](https://github.com/madpah/serializable/commit/e8b37f2ee4246794c6c0e295bcdf32cd58d5e52d)) ### Unknown * 0.6.0 Automatically generated by python-semantic-release ([`da20686`](https://github.com/madpah/serializable/commit/da20686207f0ca95f7da29cb07f27ecc018b5134)) * Merge branch 'main' of github.com:madpah/serializable ([`86492e1`](https://github.com/madpah/serializable/commit/86492e1ff51f6ecd5dde28faf054777db13fe5b1)) ## v0.5.0 (2022-09-12) ### Feature * feat: support for string formatting of values Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`99b8f3e`](https://github.com/madpah/serializable/commit/99b8f3e7ab84f087a87b330928fc598c96a0e682)) * feat: support string formatting for values Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`3fefe22`](https://github.com/madpah/serializable/commit/3fefe2294130b80f05e219bd655514a0956f7f93)) * feat: support for custom Enum implementations Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`c3622fc`](https://github.com/madpah/serializable/commit/c3622fcb0019de794b1cbd3ad6333b6044d8392a)) ### Unknown * 0.5.0 Automatically generated by python-semantic-release ([`0ede79d`](https://github.com/madpah/serializable/commit/0ede79daabcf3ce3c6364e8abc27f321db654a90)) * Merge branch 'main' of github.com:madpah/serializable ([`5a896c4`](https://github.com/madpah/serializable/commit/5a896c4f3162569e4e938cb4dd1e69275078f8ee)) * import order Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`a2a2ef8`](https://github.com/madpah/serializable/commit/a2a2ef86e2c9fe860453f755201507266c36daed)) ## v0.4.0 (2022-09-06) ### Feature * feat: add support for defining XML element ordering with `@serializable.xml_sequence()` decorator Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`c1442ae`](https://github.com/madpah/serializable/commit/c1442aeb1776243922fbaa6b5174db5a54f71920)) ### Fix * fix: removed unused dependencies Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`448a3c9`](https://github.com/madpah/serializable/commit/448a3c9f0de897cf1ee6d7c46af377c2f389730d)) * fix: handle python builtins and keywords during `as_xml()` for element names Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`3bbfb1b`](https://github.com/madpah/serializable/commit/3bbfb1b4a7808f4cedd3b2b15f31aaaf8e35d60a)) * fix: handle python builtins and keywords during `as_xml()` for attributes Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`8d6a96b`](https://github.com/madpah/serializable/commit/8d6a96b0850d4993c96cbc7d532d848ba9c5e8b3)) ### Unknown * 0.4.0 Automatically generated by python-semantic-release ([`3034bd1`](https://github.com/madpah/serializable/commit/3034bd1f817e2cc24c10da4c7d0a1d68120f1fee)) * python < 3.8 typing Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`339e53c`](https://github.com/madpah/serializable/commit/339e53cbec9a441ef9ef6ecea9f037c9085b6855)) * removed unused import Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`8462634`](https://github.com/madpah/serializable/commit/84626342df1dd5d9aea8d4c469431a0b19cf0bb3)) * updated release CI Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`f4cf0fa`](https://github.com/madpah/serializable/commit/f4cf0fa4d6a9f3349647caeb94d18b97bc836606)) * typing Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`0f9cf68`](https://github.com/madpah/serializable/commit/0f9cf68db3e676a9e16124c371359ec60e2fc304)) * cleanup Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`95a864a`](https://github.com/madpah/serializable/commit/95a864a1f9c67ec073308fdc3e97b82ce81b5392)) * test alternative poetry installation in CI Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`8eb8704`](https://github.com/madpah/serializable/commit/8eb8704f7b14767897093183020b71f6672f86c4)) * test alternative poetry installation in CI Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`8705180`](https://github.com/madpah/serializable/commit/87051801d6718c2eb4dd380e91bc30b9684a6386)) * test alternative poetry installation in CI Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`fe3f56a`](https://github.com/madpah/serializable/commit/fe3f56a26a20be4f6ccd3ae100300c947bdecf70)) * test alternative poetry installation in CI Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`4e7a19f`](https://github.com/madpah/serializable/commit/4e7a19fc54c2e51f6b963a4e9d758d0d8824413c)) * test alternative poetry installation in CI Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`7d268db`](https://github.com/madpah/serializable/commit/7d268dbad701604946877ef8e3947f8b14210f7e)) * test alternative poetry installation in CI Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`02caa9e`](https://github.com/madpah/serializable/commit/02caa9e35d3ac3a3b961b09cb9665e9f27ab1371)) * test alternative poetry installation in CI Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`210d41d`](https://github.com/madpah/serializable/commit/210d41d39418cd58af62b2672233e743dbd4372f)) * force poetry cache clear Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`731d7ae`](https://github.com/madpah/serializable/commit/731d7ae51ac7bd1225af7d3c757042cac9f3ac9c)) * bump poetry to 1.1.12 Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`90b8a92`](https://github.com/madpah/serializable/commit/90b8a92327741c5b8b91a7fb1ef1356febe53944)) * typing Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`3427f4b`](https://github.com/madpah/serializable/commit/3427f4b5b136183b524cda871fb49f9ab78a20a7)) * doc: added docs for `xml_sequence()` decorator Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`d2211c9`](https://github.com/madpah/serializable/commit/d2211c90b65e27510711d90daf1b001f3e7c81e2)) * Merge branch 'main' of github.com:madpah/serializable ([`6520862`](https://github.com/madpah/serializable/commit/652086249f399f8592fc89ee6fcb33ebdbe6973d)) * namespacing for XML output Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`2e32f08`](https://github.com/madpah/serializable/commit/2e32f084552bee69ad815466741d66fee96ff2e1)) ## v0.3.9 (2022-08-24) ### Fix * fix: support declaration of XML NS in `as_xml()` call Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`19b2b70`](https://github.com/madpah/serializable/commit/19b2b7048fdd7048d62f618987c13f2d3a457726)) ### Unknown * 0.3.9 Automatically generated by python-semantic-release ([`3269921`](https://github.com/madpah/serializable/commit/32699214554b0ec5d4b592f2ab70d6ae923c9e9c)) ## v0.3.8 (2022-08-24) ### Fix * fix: deserialization of XML boolean values Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`799d477`](https://github.com/madpah/serializable/commit/799d4773d858fdf8597bef905302a373ca150db8)) ### Unknown * 0.3.8 Automatically generated by python-semantic-release ([`dbf545c`](https://github.com/madpah/serializable/commit/dbf545cb4a51a10125a4104771ecca11e484ac53)) ## v0.3.7 (2022-08-23) ### Fix * fix: fixed deferred parsing for Properties Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`833e29b`](https://github.com/madpah/serializable/commit/833e29b8391c85931b12c98f87a2faf3a68d388e)) ### Unknown * 0.3.7 Automatically generated by python-semantic-release ([`1628f28`](https://github.com/madpah/serializable/commit/1628f2870c8de2643c74550cbe34c09d84b419d7)) ## v0.3.6 (2022-08-23) ### Fix * fix: support for cyclic dependencies Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`911626c`](https://github.com/madpah/serializable/commit/911626c88fb260049fdf2931f6ea1b0b05d7166a)) ### Unknown * 0.3.6 Automatically generated by python-semantic-release ([`54607f1`](https://github.com/madpah/serializable/commit/54607f1ac9e64e7cd8762699fd7f1567ac9c8d83)) * Merge branch 'main' of github.com:madpah/serializable ([`a54d5cf`](https://github.com/madpah/serializable/commit/a54d5cf3a68959f006340e88fc2f095558a70b1a)) ## v0.3.5 (2022-08-22) ### Fix * fix: support for non-primitive types when XmlSerializationType == FLAT Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`7eff15b`](https://github.com/madpah/serializable/commit/7eff15bbb8d20760418071c005d65d2623b44eab)) ### Unknown * 0.3.5 Automatically generated by python-semantic-release ([`d7e03d1`](https://github.com/madpah/serializable/commit/d7e03d13522d983ab79e4fa114f5deb4d43a7db9)) * Merge branch 'main' of github.com:madpah/serializable ([`6ec8c38`](https://github.com/madpah/serializable/commit/6ec8c38219e392ecab25d9eee7b67b05cc3b85f2)) ## v0.3.4 (2022-08-22) ### Fix * fix: support ENUM in XML Attributes Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`f2f0922`](https://github.com/madpah/serializable/commit/f2f0922f2d0280185f6fc7f96408d6647588c8d2)) ### Unknown * 0.3.4 Automatically generated by python-semantic-release ([`adae34c`](https://github.com/madpah/serializable/commit/adae34c2c7be2ab920335d038cc4f9a80dbb128f)) * Merge branch 'main' of github.com:madpah/serializable ([`8995505`](https://github.com/madpah/serializable/commit/899550591954f4236bbeb53191d6ad47cdf8779d)) * code styling Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`7ec0197`](https://github.com/madpah/serializable/commit/7ec01978b2b581b0fbeb610b0707d4d6aa42ec1a)) ## v0.3.3 (2022-08-19) ### Fix * fix: handle Array types where the concrete type is quoted in its definition Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`b6db879`](https://github.com/madpah/serializable/commit/b6db879d72822ada74a41362594b009f09349da9)) ### Unknown * 0.3.3 Automatically generated by python-semantic-release ([`f0c463b`](https://github.com/madpah/serializable/commit/f0c463b45061b05e060df526185c3b374f49fda2)) * Merge branch 'main' of github.com:madpah/serializable ([`ea0aa86`](https://github.com/madpah/serializable/commit/ea0aa86cbba7b8504e52dcabc8f781af81326d82)) ## v0.3.2 (2022-08-19) ### Fix * fix: work to support `sortedcontainers` as a return type for Properties Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`805a3f7`](https://github.com/madpah/serializable/commit/805a3f7a10e41f63b132ac0bb234497d5d39fe2b)) ### Unknown * 0.3.2 Automatically generated by python-semantic-release ([`f86da94`](https://github.com/madpah/serializable/commit/f86da944467b0d8ff571f3ca2e924b50e388bb4c)) * Merge branch 'main' of github.com:madpah/serializable ([`cf9234e`](https://github.com/madpah/serializable/commit/cf9234e65c55b3d1814c36c7b3c2dcfb9b4ae1d5)) ## v0.3.1 (2022-08-19) ### Fix * fix: better support for Properties that have a Class type that is not a Serializable Class (e.g. UUID) Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`95d407b`](https://github.com/madpah/serializable/commit/95d407b4456d8f106cf54ceb650cbde1aab69457)) ### Unknown * 0.3.1 Automatically generated by python-semantic-release ([`53d96bd`](https://github.com/madpah/serializable/commit/53d96bd515bf4bafa1216bc6041e25b8f7ddecb7)) ## v0.3.0 (2022-08-19) ### Feature * feat: support ignoring elements/properties during deserialization Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`6319d1f`](https://github.com/madpah/serializable/commit/6319d1f9e632a941b1d79a63083c1ecb194105be)) ### Unknown * 0.3.0 Automatically generated by python-semantic-release ([`a286b88`](https://github.com/madpah/serializable/commit/a286b88a5a9cb17eaa4f04c94f9c0c148e9e7052)) ## v0.2.3 (2022-08-19) ### Fix * fix: update `helpers` to be properly typed Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`d924dc2`](https://github.com/madpah/serializable/commit/d924dc2d3b5f02c61ff6ac36fa10fa6adaac7022)) ### Unknown * 0.2.3 Automatically generated by python-semantic-release ([`f632d2f`](https://github.com/madpah/serializable/commit/f632d2f10b7b5fb6cbdad038eaacaf73c2c9bbb7)) * Merge branch 'main' of github.com:madpah/serializable ([`5d6564d`](https://github.com/madpah/serializable/commit/5d6564d787f8b269b86f3a5c4f055c62c38fd676)) ## v0.2.2 (2022-08-19) ### Fix * fix: change to helpers to address typing issues Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`1c32ba1`](https://github.com/madpah/serializable/commit/1c32ba143504a605a77df4908422a95d0bd07edf)) * fix: remove `/` from method signature so we work on Python < 3.8 Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`c45864c`](https://github.com/madpah/serializable/commit/c45864cd6c90ed38d8cedd944adcfe43b32326b2)) ### Unknown * 0.2.2 Automatically generated by python-semantic-release ([`60045d8`](https://github.com/madpah/serializable/commit/60045d8342357b0a3ffe6b2a22abc9068f0d140c)) ## v0.2.1 (2022-08-18) ### Fix * fix: update to work on python < 3.10 Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`91df8cb`](https://github.com/madpah/serializable/commit/91df8cbb718db15ea182888aa796db32b8015004)) ### Unknown * 0.2.1 Automatically generated by python-semantic-release ([`4afc403`](https://github.com/madpah/serializable/commit/4afc4035f5dda5e6387963abb8d1332aa90dbd2c)) * Merge branch 'main' of github.com:madpah/serializable ([`dbc5039`](https://github.com/madpah/serializable/commit/dbc50397638e4a738443c6a3b5b809d64d962ddf)) ## v0.2.0 (2022-08-18) ### Feature * feat: library re-write to utilise decorators Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`957fca7`](https://github.com/madpah/serializable/commit/957fca757d89dc1b8ef9b13357a5a9380dbe94ff)) ### Unknown * 0.2.0 Automatically generated by python-semantic-release ([`5bff0a8`](https://github.com/madpah/serializable/commit/5bff0a88ecc4c135ec60eafcc592f55157e1b103)) * Merge branch 'main' of github.com:madpah/serializable ([`b14e2c9`](https://github.com/madpah/serializable/commit/b14e2c9f8da270318fe1fddf242c82570027729d)) * doc changes to reflect move to use of decorators Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`9f1b4ca`](https://github.com/madpah/serializable/commit/9f1b4ca17ee57f8a55ae211d78daed29c0068584)) * typing Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`d3633ed`](https://github.com/madpah/serializable/commit/d3633ed1fc09b72ea222a51b4a852dd7db52a0bf)) * typing Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`3480d71`](https://github.com/madpah/serializable/commit/3480d7126e063cef5746522479b381eba8cca818)) * removed `print()` calls - added logger Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`1deee5e`](https://github.com/madpah/serializable/commit/1deee5ec611a3c31f63a66be762caac70625472f)) * removed dead code Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`375b367`](https://github.com/madpah/serializable/commit/375b367e8705b5b6d0b5e4ac0c506776eb9da001)) * wip: all unit tests passing for JSON and XML after migrating to use of decorators Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`d4ab8f4`](https://github.com/madpah/serializable/commit/d4ab8f413b1f2bbf79e5a66ea353407f9dc15944)) * wip - JSON serialization and deserialization unit tests passing using decorators Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`291b37d`](https://github.com/madpah/serializable/commit/291b37da7d3f414750d555797f24378158eae4c4)) * wip - move to using Decorators to annotate classes to affect serialization/deserialization Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`05d6e5a`](https://github.com/madpah/serializable/commit/05d6e5a68630e4af09e81a02d5aca4a55391871a)) ## v0.1.7 (2022-08-15) ### Fix * fix: support for Objects that when represented in XML may just be simple elements with attributes Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`1369d7d`](https://github.com/madpah/serializable/commit/1369d7d755d9e50273b72e2fdd7d2967442e5bde)) ### Unknown * 0.1.7 Automatically generated by python-semantic-release ([`291a2b3`](https://github.com/madpah/serializable/commit/291a2b3822e2f5c0e4b1ed7c90b3205147f74704)) * Merge branch 'main' of github.com:madpah/serializable ([`9c34c2f`](https://github.com/madpah/serializable/commit/9c34c2fe5d4dd96e04b54949b2b3bbd088ac9ca1)) ## v0.1.6 (2022-08-15) ### Fix * fix: temporarilty add `Any` as part of `AnySerializable` type Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`d3e9beb`](https://github.com/madpah/serializable/commit/d3e9bebd7b8dc78d4eb36447ad0b1ee46e2745e0)) ### Unknown * 0.1.6 Automatically generated by python-semantic-release ([`77cc49b`](https://github.com/madpah/serializable/commit/77cc49bd1ad9fae4bed17eaf47659d584a3cec3f)) ## v0.1.5 (2022-08-13) ### Fix * fix: direct support for Python `Enum` Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`50148cc`](https://github.com/madpah/serializable/commit/50148cc98a26e4e51479b491acb58451ea5b42b6)) ### Unknown * 0.1.5 Automatically generated by python-semantic-release ([`532d0d1`](https://github.com/madpah/serializable/commit/532d0d1eb613d0c62e881cd898e5f5195a506b17)) ## v0.1.4 (2022-08-13) ### Fix * fix: added missing `py.typed` marker Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`ee3169f`](https://github.com/madpah/serializable/commit/ee3169f466353a88922174b40f5b29cb98998be9)) ### Unknown * 0.1.4 Automatically generated by python-semantic-release ([`02c2c30`](https://github.com/madpah/serializable/commit/02c2c3019de4939138c92d070ffdadb86d9dc7f4)) * Merge branch 'main' of github.com:madpah/serializable ([`5219023`](https://github.com/madpah/serializable/commit/5219023246373e5e98663d46736c2299fc77b548)) ## v0.1.3 (2022-08-12) ### Fix * fix: added helpers for serializing XML dates and times (xsd:date, xsd:datetime) Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`c309834`](https://github.com/madpah/serializable/commit/c3098346abf445876d99ecb768d7a4a08b12a291)) ### Unknown * 0.1.3 Automatically generated by python-semantic-release ([`9c6de39`](https://github.com/madpah/serializable/commit/9c6de399dd9ea70b2136a8aa0797a3bd3ffbc881)) * Merge branch 'main' of github.com:madpah/serializable ([`986286f`](https://github.com/madpah/serializable/commit/986286f9723b9a2154b0e3d9d5d7d14f64f65c8a)) ## v0.1.2 (2022-08-12) ### Fix * fix: support for properties whose value is an `Type[SerializableObject]` but are not `List` or `Set` Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`bf6773c`](https://github.com/madpah/serializable/commit/bf6773c40f3f45dbe2821fdbe785b369f0b3b71c)) ### Unknown * 0.1.2 Automatically generated by python-semantic-release ([`7ca1b6f`](https://github.com/madpah/serializable/commit/7ca1b6f92061c8cd73d8554c764dc4b39c2b6364)) * Merge branch 'main' of github.com:madpah/serializable ([`bdb75e0`](https://github.com/madpah/serializable/commit/bdb75e0961cc17b11abb37dd984af16c0623d18f)) ## v0.1.1 (2022-08-11) ### Fix * fix: handle nested objects that are not list or set Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`4bc5252`](https://github.com/madpah/serializable/commit/4bc525258d0ee655beabace18e41323b4b67ae1b)) ### Unknown * 0.1.1 Automatically generated by python-semantic-release ([`fc77999`](https://github.com/madpah/serializable/commit/fc77999d8ab8c8ac2f6273f64387f95104551e56)) ## v0.1.0 (2022-08-10) ### Feature * feat: first alpha release Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`c95a772`](https://github.com/madpah/serializable/commit/c95a7724186b6e45554624b5238c719d172ffc9f)) * feat: first working draft of library for (de-)serialization to/from JSON and XML Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`7af4f9c`](https://github.com/madpah/serializable/commit/7af4f9c4a100f1ce10502ecef228f42ea61e9c22)) ### Unknown * 0.1.0 Automatically generated by python-semantic-release ([`701a522`](https://github.com/madpah/serializable/commit/701a522410783677087a0da682f899f4fbd4368d)) * doc: fixed doc config and added first pass documentation Signed-off-by: Paul Horton <paul.horton@owasp.org> ([`38705a1`](https://github.com/madpah/serializable/commit/38705a1156d04a5ae5fc96c6cd691e1d1a0e2ead)) * Initial commit ([`70ca2a5`](https://github.com/madpah/serializable/commit/70ca2a5d8d4042c969e1120e5604ea37878ac5c3)) serializable-2.0.0/CODEOWNERS000066400000000000000000000014641475213023200155560ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. # see https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners * @madpah @jkowalleckserializable-2.0.0/CONTRIBUTING.md000066400000000000000000000027061475213023200164140ustar00rootroot00000000000000# Contributing Pull requests are welcome, but please read this guidelines first. ## Setup This project uses [poetry]. Have it installed and setup first. Attention: Even though this library is designed to be runnable on python>=3.8.0 some development-tools require python>=3.8.1 To install dev-dependencies and tools: ```shell poetry install ``` ## Code style This project uses [PEP8] Style Guide for Python Code. This project loves sorted imports. Get it all applied via: ```shell poetry run isort . poetry run autopep8 -ir py_serializable/ tests/ ``` This project prefers `f'strings'` over `'string'.format()`. This project prefers `'single quotes'` over `"double quotes"`. ## Documentation This project uses [Sphinx] to generate documentation which is automatically published to [RTFD][link_rtfd]. Source for documentation is stored in the `docs` folder in [RST] format. You can generate the documentation locally by running: ```shell cd docs pip install -r requirements.txt make html ``` ## Testing ```shell poetry run tox ``` ## Sign your commits Please sign your commits, to show that you agree to publish your changes under the current terms and licenses of the project. ```shell git commit --signed-off ... ``` [poetry]: https://python-poetry.org [PEP8]: https://www.python.org/dev/peps/pep-0008/ [Sphinx]: https://www.sphinx-doc.org/ [link_rtfd]: https://py-serializable.readthedocs.io/ [RST]: https://en.wikipedia.org/wiki/ReStructuredText serializable-2.0.0/LICENSE000066400000000000000000000261351475213023200151720ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. serializable-2.0.0/README.md000066400000000000000000000055241475213023200154430ustar00rootroot00000000000000# py-serializable [![shield_pypi-version]][link_pypi] [![shield_conda-forge-version]][link_conda-forge] [![shield_rtfd]][link_rtfd] [![shield_gh-workflow-test]][link_gh-workflow-test] [![shield_license]][license_file] [![shield_twitter-follow]][link_twitter] ---- This Pythonic library provides a framework for serializing/deserializing Python classes to and from JSON and XML. It relies upon the use of [Python Properties](https://docs.python.org/3/library/functions.html?highlight=property#property) in your Python classes. Read the full [documentation][link_rtfd] for more details. ## Installation Install this from [PyPi.org][link_pypi] using your preferred Python package manager. Example using `pip`: ```shell pip install py-serializable ``` Example using `poetry`: ```shell poetry add py-serializable ``` ## Usage See the full [documentation][link_rtfd] or our [unit tests][link_unit_tests] for usage and details. ## Python Support We endeavour to support all functionality for all [current actively supported Python versions](https://www.python.org/downloads/). However, some features may not be possible/present in older Python versions due to their lack of support. ## Contributing Feel free to open issues, bugreports or pull requests. See the [CONTRIBUTING][contributing_file] file for details. ## Copyright & License `py-serializable` is Copyright (c) Paul Horton 2022. All Rights Reserved. Permission to modify and redistribute is granted under the terms of the Apache 2.0 license. See the [LICENSE][license_file] file for the full license. [license_file]: https://github.com/madpah/serializable/blob/main/LICENSE [contributing_file]: https://github.com/madpah/serializable/blob/main/CONTRIBUTING.md [link_rtfd]: https://py-serializable.readthedocs.io/ [shield_gh-workflow-test]: https://img.shields.io/github/actions/workflow/status/madpah/serializable/python.yml?branch=main "build" [shield_rtfd]: https://img.shields.io/readthedocs/py-serializable?logo=readthedocs&logoColor=white [shield_pypi-version]: https://img.shields.io/pypi/v/py-serializable?logo=Python&logoColor=white&label=PyPI "PyPI" [shield_conda-forge-version]: https://img.shields.io/conda/vn/conda-forge/py-serializable?logo=anaconda&logoColor=white&label=conda-forge "conda-forge" [shield_license]: https://img.shields.io/github/license/madpah/serializable?logo=open%20source%20initiative&logoColor=white "license" [shield_twitter-follow]: https://img.shields.io/badge/Twitter-follow-blue?logo=Twitter&logoColor=white "twitter follow" [link_gh-workflow-test]: https://github.com/madpah/serializable/actions/workflows/python.yml?query=branch%3Amain [link_pypi]: https://pypi.org/project/py-serializable/ [link_conda-forge]: https://anaconda.org/conda-forge/py-serializable [link_twitter]: https://twitter.com/madpah [link_unit_tests]: https://github.com/madpah/serializable/blob/main/tests serializable-2.0.0/SECURITY.md000066400000000000000000000013101475213023200157420ustar00rootroot00000000000000# Security Policy ## Reporting a Vulnerability To report a vulnerability, please open an issue here: In case of a security issue, please do not disclose publicly, until it is fixed, just mention this fact in an issue, and we will contact you with information on which mehtod/channel to communicate. serializable-2.0.0/docs/000077500000000000000000000000001475213023200151065ustar00rootroot00000000000000serializable-2.0.0/docs/Makefile000066400000000000000000000011721475213023200165470ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) serializable-2.0.0/docs/changelog.rst000066400000000000000000000014021475213023200175640ustar00rootroot00000000000000.. # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. .. mdinclude:: ../CHANGELOG.mdserializable-2.0.0/docs/conf.py000066400000000000000000000046771475213023200164230ustar00rootroot00000000000000# This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. # -- Project information ----------------------------------------------------- project = 'py-serializable' copyright = '2022-Present Paul Horton' author = 'Paul Horton' # The full version, including alpha/beta/rc tags # !! version is managed by semantic_release release = "2.0.0" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", # "sphinx.ext.viewcode", "autoapi.extension", "sphinx_rtd_theme", "m2r2" ] # Document Python Code autoapi_type = 'python' autoapi_dirs = ['../py_serializable'] # see https://sphinx-autoapi.readthedocs.io/en/latest/reference/config.html#confval-autoapi_options autoapi_options = ['show-module-summary', 'members', 'undoc-members', 'inherited-members', 'show-inheritance'] source_suffix = ['.rst', '.md'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.venv'] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] serializable-2.0.0/docs/customising-structure.rst000066400000000000000000000230321475213023200222420ustar00rootroot00000000000000.. # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. Customising Serialization ==================================================== There are various scenarios whereby you may want to have more control over the structure (particularly in XML) that is generated when serializing an object, and thus understanding how to deserialize JSON or XML back to an object. This library provides a number of *meta methods* that you can override in your Python classes to achieve this. Property Name Mappings ---------------------------------------------------- You can directly control mapping of property names for properties in a Class by adding the decorators :func:`py_serializable.json_name()` or :func:`py_serializable.xml_name()`. For example, you might have a property called **isbn** in your class, but when serialized to JSON it should be called **isbn_number**. To implement this mapping, you would alter your class as follows adding the :func:`py_serializable.json_name()` decorator to the **isbn** property: .. code-block:: python @py_serializable.serializable_class class Book: def __init__(self, title: str, isbn: str, publish_date: date, authors: Iterable[str], ... @property @py_serializable.json_name('isbn_number') def isbn(self) -> str: return self._isbn Excluding Property from Serialization ---------------------------------------------------- Properties can be ignored during deserialization by including them in the :func:`py_serializable.serializable_class()` annotation as per the following example. A typical use case for this might be where a JSON schema is referenced, but this is not part of the constructor for the class you are deserializing to. .. code-block:: python @py_serializable.serializable_class(ignore_during_deserialization=['$schema']) class Book: ... Handling ``None`` Values ---------------------------------------------------- By default, ``None`` values will lead to a Property being excluded from the serialization process to keep the output as concise as possible. There are many cases (and schemas) where this is however not the required behaviour. You can force a Property to be serialized even when the value is ``None`` by annotating as follows: .. code-block:: python @py_serializable.include_none def email(self) -> Optional[str]: return self._email Customised Property Serialization ---------------------------------------------------- This feature allows you to handle, for example, serialization of :class:`datetime.date` Python objects to and from strings. Depending on your use case, the string format could vary, and thus this library makes no assumptions. We have provided an some example helpers for (de-)serializing dates and datetimes. To define a custom serializer for a property, add the :func:`py_serializable.type_mapping()` decorator to the property. For example, to have a property named *created* be use the :class:`py_serializable.helpers.Iso8601Date` helper you would add the following method to your class: .. code-block:: python @py_serializable.serializable_class class Book: def __init__(self, title: str, isbn: str, publish_date: date, authors: Iterable[str], ... @property @py_serializable.type_mapping(Iso8601Date) def publish_date(self) -> date: return self._publish_date Writing Custom Property Serializers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can write your own custom property serializer. The only requirements are that it must extend :class:`py_serializable.helpers.BaseHelper` and therefore implement the ``serialize()`` and ``deserialize()`` class methods. For examples, see :mod:`py_serializable.helpers`. Serializing Lists & Sets ---------------------------------------------------- Particularly in XML, there are many ways that properties which return Lists or Sets could be represented. We can handle this by adding the decorator :func:`py_serializable.xml_array()` to the appropriate property in your class. For example, given a Property that returns ``Set[Chapter]``, this could be serialized in one of a number of ways: .. code-block:: json :caption: Example 1: Nested list under a property name in JSON { "chapters": [ { /* chapter 1 here... */ }, { /* chapter 2 here... */ }, // etc... ] } .. code-block:: xml :caption: Example 2: Nested list under a property name in XML .. code-block:: xml :caption: Example 3: Collapsed list under a (potentially singular of the) property name in XML .. note: Other structures may also be possible, but only the above are considered by this library at the current time. As we have only identified one possible structure for JSON at this time, the implementation of only affects XML (de-)serialization at this time. For *Example 2*, you would add the following to your class: .. code-block:: python @property @py_serializable.xml_array(XmlArraySerializationType.NESTED, 'chapter') def chapters(self) -> List[Chapter]: return self._chapters For *Example 3*, you would add the following to your class: .. code-block:: python @property @py_serializable.xml_array(XmlArraySerializationType.FLAT, 'chapter') def chapters(self) -> List[Chapter]: return self._chapters Further examples are available in our :ref:`unit tests `. Serializing special XML string types ---------------------------------------------------- In XML, are special string types, ech with defined set of allowed characters and whitespace handling. We can handle this by adding the decorator :obj:`py_serializable.xml_string()` to the appropriate property in your class. .. code-block:: python @property @py_serializable.xml_string(py_serializable.XmlStringSerializationType.TOKEN) def author(self) -> str: return self._author Further examples are available in our :ref:`unit tests `. .. note:: The actual transformation is done by :func:`py_serializable.xml.xs_normalizedString()` and :func:`py_serializable.xml.xs_token()` Serialization Views ---------------------------------------------------- Many object models can be serialized to and from multiple versions of a schema or different schemas. In ``py-serialization`` we refer to these as Views. By default all Properties will be included in the serialization process, but this can be customised based on the View. Defining Views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A View is a class that extends :class:`py_serializable.ViewType` and you should create classes as required in your implementation. For example: .. code-block:: python from py_serializable import ViewType class SchemaVersion1(ViewType): pass Property Inclusion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Properties can be annotated with the Views for which they should be included. For example: .. code-block:: python @property @py_serializable.view(SchemaVersion1) def address(self) -> Optional[str]: return self._address Handling ``None`` Values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Further to the above, you can vary the ``None`` value per View as follows: .. code-block:: python @property @py_serializable.include_none(SchemaVersion2) @py_serializable.include_none(SchemaVersion3, "RUBBISH") def email(self) -> Optional[str]: return self._email The above example will result in ``None`` when serializing with the View ``SchemaVersion2``, but the value ``RUBBISH`` when serializing to the View ``SchemaVersion3`` when ``email`` is not set. Serializing For a View ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To serialized for a specific View, include the View when you perform the serialisation. .. code-block:: python :caption: JSON Example ThePhoenixProject.as_json(view_=SchemaVersion1) .. code-block:: python :caption: XML Example ThePhoenixProject.as_xml(view_=SchemaVersion1) XML Element Ordering ---------------------------------------------------- Some XML schemas utilise `sequence`_ which requires elements to be in a prescribed order. You can control the order properties are serialized to elements in XML by utilising the :func:`py_serializable.xml_sequence()` decorator. The default sort order applied to properties is 100 (where lower is earlier in the sequence). In the example below, the ``isbn`` property will be output first. .. code-block:: python @property @py_serializable.xml_sequence(1) def isbn(self) -> str: return self._isbn .. _sequence: https://www.w3.org/TR/xmlschema-0/#element-sequence serializable-2.0.0/docs/examples.rst000066400000000000000000000035731475213023200174660ustar00rootroot00000000000000.. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 Examples ======== .. _unit-tests: Models used in Unit Tests ------------------------- .. literalinclude:: ../tests/model.py :language: python :linenos: Logging and log access ---------------------- This library utilizes an own instance of `Logger`_, which you may access and add handlers to. .. _Logger: https://docs.python.org/3/library/logging.html#logger-objects .. code-block:: python :caption: Example: send all logs messages to stdErr import sys import logging import py_serializable my_log_handler = logging.StreamHandler(sys.stderr) my_log_handler.setLevel(logging.DEBUG) my_log_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) py_serializable.logger.addHandler(my_log_handler) py_serializable.logger.setLevel(my_log_handler.level) py_serializable.logger.propagate = False @py_serializable.serializable_class class Chapter: def __init__(self, *, number: int, title: str) -> None: self._number = number self._title = title @property def number(self) -> int: return self._number @property def title(self) -> str: return self._title moby_dick_c1 = Chapter(number=1, title='Loomings') print(moby_dick_c1.as_json()) serializable-2.0.0/docs/formatters.rst000066400000000000000000000055561475213023200200410ustar00rootroot00000000000000.. # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. Property Name Formatting ==================================================== By default, ``py-serializable`` uses it's :class:`py_serializable.formatters.CamelCasePropertyNameFormatter` formatter for translating actual Python property names to element names in either JSON or XML. ``py-serializable`` includes a number of name formatters out of the box, but you can also create your own if required. Included Formatters ---------------------------------------------------- ``py-serializable`` includes three common formatters out of the box. 1. Camel Case Formatter: :class:`py_serializable.formatters.CamelCasePropertyNameFormatter` (the default) 2. Kebab Case Formatter: :class:`py_serializable.formatters.KebabCasePropertyNameFormatter` 3. Snake Case Formatter: :class:`py_serializable.formatters.SnakeCasePropertyNameFormatter` A summary of how these differ is included in the below table. +----------------------------+---------------+----------------+-----------------+ | Python Property Name | Camel Case | Kebab Case | Snake Case | +============================+===============+================+=================+ | books | books | books | books | +----------------------------+---------------+----------------+-----------------+ | big_book | bigBook | big-book | big_book | +----------------------------+---------------+----------------+-----------------+ | a_very_big_book | aVeryBigBook | a-very-big-book| a_very_big_book | +----------------------------+---------------+----------------+-----------------+ Changing the Formatter ---------------------- You can change the formatter being used by easily. The example below changes the formatter to be Snake Case. .. code-block:: python from py_serializable.formatters import CurrentFormatter, SnakeCasePropertyNameFormatter CurrentFormatter.formatter = SnakeCasePropertyNameFormatter Custom Formatters ----------------- If none of the included formatters work for you, why not write your own? The only requirement is that it extends :class:`py_serializable.formatters.BaseNameFormatter`! serializable-2.0.0/docs/getting-started.rst000066400000000000000000000117741475213023200207570ustar00rootroot00000000000000.. # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. Getting Started ==================================================== Let's work a simple example together. I have a two Python classes that together I use to model Books. They are ``Book`` and ``Chapter``, and they are defined as follows: .. code-block:: python class Chapter: def __init__(self, *, number: int, title: str) -> None: self._number = number self._title = title @property def number(self) -> int: return self._number @property def title(self) -> str: return self._title class Book: def __init__(self, *, title: str, isbn: str, edition: int, publish_date: date, authors: Iterable[str], chapters: Optional[Iterable[Chapter]] = None) -> None: self._title = title self._isbn = isbn self._edition = edition self._publish_date = publish_date self._authors = set(authors) self.chapters = chapters or [] @property def title(self) -> str: return self._title @property def isbn(self) -> str: return self._isbn @property def edition(self) -> int: return self._edition @property def publish_date(self) -> date: return self._publish_date @property def authors(self) -> Set[str]: return self._authors @property def chapters(self) -> List[Chapter]: return self._chapters @chapters.setter def chapters(self, chapters: Iterable[Chapter]) -> None: self._chapters = list(chapters) To make a class serializable to/from JSON or XML, the class must be annotated with the decorator :func:`py_serializable.serializable_class`. By simply modifying the classes above, we make them (de-)serializable with this library (albeit with some default behaviour implied!). This makes our classes: .. code-block:: python import py_serializable @py_serializable.serializable_class class Chapter: def __init__(self, *, number: int, title: str) -> None: self._number = number self._title = title @property def number(self) -> int: return self._number @property def title(self) -> str: return self._title @py_serializable.serializable_class class Book: def __init__(self, *, title: str, isbn: str, edition: int, publish_date: date, authors: Iterable[str], chapters: Optional[Iterable[Chapter]] = None) -> None: self._title = title self._isbn = isbn self._edition = edition self._publish_date = publish_date self._authors = set(authors) self.chapters = chapters or [] @property def title(self) -> str: return self._title @property def isbn(self) -> str: return self._isbn @property def edition(self) -> int: return self._edition @property def publish_date(self) -> date: return self._publish_date @property def authors(self) -> Set[str]: return self._authors @property def chapters(self) -> List[Chapter]: return self._chapters @chapters.setter def chapters(self, chapters: Iterable[Chapter]) -> None: self._chapters = list(chapters) At this point, we can serialize an instance of ``Book`` to JSON as follows: .. code-block:: python book = Book(title="My Book", isbn="999-888777666555", edition=1, publish_date=datetime.utcnow(), authors=['me']) print(book.as_json()) which outputs: .. code-block:: json { "title": "My Book", "isbn": "999-888777666555", "edition": 1, "publishDate": "2022-08-10", "authors": [ "me" ] } We could also serialized to XML as follows: .. code-block:: python print(book.as_xml()) which outputs: .. code-block:: xml My Book 999-888777666555 1 2022-08-10 me serializable-2.0.0/docs/index.rst000066400000000000000000000030071475213023200167470ustar00rootroot00000000000000.. # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. py-serializable Documentation ==================================================== This Pythonic-library can be used to magically handle serialization of your Python Objects to JSON or XML and de-serialization from JSON or XML back to Pythonic Object instances. This library relies upon your Python Classes utilising the `@property`_ decorator and can optionally take additional configuration which allows you to control how a class is (de-)serialized. See also: - Python's `property()`_ function/decorator .. toctree:: :maxdepth: 2 :caption: Contents: getting-started customising-structure formatters examples support changelog migration .. _@property: https://realpython.com/python-property/ .. _property(): https://docs.python.org/3/library/functions.html#property serializable-2.0.0/docs/make.bat000066400000000000000000000014401475213023200165120ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd serializable-2.0.0/docs/migration.rst000066400000000000000000000033221475213023200176310ustar00rootroot00000000000000.. # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. Migration ========= .. _v1_v2: From v1 to v2 ------------- The python package was renamed from ``serializable`` to ``py_serializable``. Therefore, you need to adjust your imports. The following shows a quick way to adjust imports in the most efficient way. .. code-block:: python :caption: OLD imports import serializable from serializable import ViewType, XmlArraySerializationType, XmlStringSerializationType from serializable.helpers import BaseHelper, Iso8601Date .. code-block:: python :caption: ADJUSTED imports import py_serializable as serializable from py_serializable import ViewType, XmlArraySerializationType, XmlStringSerializationType from py_serializable.helpers import BaseHelper, Iso8601Date Also, you might need to adjust the logger access: .. code-block:: python :caption: OLD logger access s_logger = logging.getLogger('serializable') .. code-block:: python :caption: NEW logger access s_logger = logging.getLogger('py_serializable') serializable-2.0.0/docs/requirements.txt000066400000000000000000000001221475213023200203650ustar00rootroot00000000000000 m2r2>=0.3.2 sphinx>=7.2.6,<9 sphinx-autoapi>=3.0.0,<4 sphinx-rtd-theme>=2.0.0,<3 serializable-2.0.0/docs/support.rst000066400000000000000000000026401475213023200173560ustar00rootroot00000000000000.. # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. Support ======= If you run into issues utilising this library, please raise a `GitHub Issue`_. When raising an issue please include as much detail as possible including: * Version of ``py-serializable`` you have installed * Input(s) * Expected Output(s) * Actual Output(s) Python Version Support ---------------------- We endeavour to support all functionality for all `current actively supported Python versions`_. However, some features may not be possible/present in older Python versions due to their lack of support - which are noted below. .. _GitHub Issue: https://github.com/madpah/py-serializable/issues .. _current actively supported Python versions: https://www.python.org/downloads/serializable-2.0.0/py_serializable/000077500000000000000000000000001475213023200173345ustar00rootroot00000000000000serializable-2.0.0/py_serializable/__init__.py000066400000000000000000001671661475213023200214660ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. from copy import copy from decimal import Decimal from enum import Enum, EnumMeta, unique from inspect import getfullargspec, getmembers, isclass from io import StringIO, TextIOBase from json import JSONEncoder, dumps as json_dumps from logging import NullHandler, getLogger from re import compile as re_compile, search as re_search from typing import ( Any, Callable, Dict, Iterable, List, Literal, Optional, Protocol, Set, Tuple, Type, TypeVar, Union, cast, overload, ) from xml.etree.ElementTree import Element, SubElement from defusedxml import ElementTree as SafeElementTree # type:ignore[import-untyped] from .formatters import BaseNameFormatter, CurrentFormatter from .helpers import BaseHelper from .xml import xs_normalizedString, xs_token # `Intersection` is still not implemented, so it is interim replaced by Union for any support # see section "Intersection" in https://peps.python.org/pep-0483/ # see https://github.com/python/typing/issues/213 from typing import Union as Intersection # isort: skip # MUST import the whole thing to get some eval/hacks working for dynamic type detection. import typing # noqa: F401 # isort: skip # !! version is managed by semantic_release # do not use typing here, or else `semantic_release` might have issues finding the variable __version__ = '2.0.0' _logger = getLogger(__name__) _logger.addHandler(NullHandler()) # make `logger` publicly available, as stable API logger = _logger """ The logger. The thing that captures all this package has to say. Feel free to modify its level and attach handlers to it. """ class ViewType: """Base of all views.""" pass _F = TypeVar('_F', bound=Callable[..., Any]) _T = TypeVar('_T') _E = TypeVar('_E', bound=Enum) @unique class SerializationType(str, Enum): """ Enum to define the different formats supported for serialization and deserialization. """ JSON = 'JSON' XML = 'XML' # tuple = immutable collection -> immutable = prevent unexpected modifications _DEFAULT_SERIALIZATION_TYPES: Iterable[SerializationType] = ( SerializationType.JSON, SerializationType.XML, ) @unique class XmlArraySerializationType(Enum): """ Enum to differentiate how array-type properties (think Iterables) are serialized. Given a ``Warehouse`` has a property ``boxes`` that returns `List[Box]`: ``FLAT`` would allow for XML looking like: `` ..box 1.. ..box 2.. `` ``NESTED`` would allow for XML looking like: `` ..box 1.. ..box 2.. `` """ FLAT = 1 NESTED = 2 @unique class XmlStringSerializationType(Enum): """ Enum to differentiate how string-type properties are serialized. """ STRING = 1 """ as raw string. see https://www.w3.org/TR/xmlschema-2/#string """ NORMALIZED_STRING = 2 """ as `normalizedString`. see http://www.w3.org/TR/xmlschema-2/#normalizedString""" TOKEN = 3 """ as `token`. see http://www.w3.org/TR/xmlschema-2/#token""" # unimplemented cases # - https://www.w3.org/TR/xmlschema-2/#language # - https://www.w3.org/TR/xmlschema-2/#NMTOKEN # - https://www.w3.org/TR/xmlschema-2/#Name # region _xs_string_mod_apply __XS_STRING_MODS: Dict[XmlStringSerializationType, Callable[[str], str]] = { XmlStringSerializationType.NORMALIZED_STRING: xs_normalizedString, XmlStringSerializationType.TOKEN: xs_token, } def _xs_string_mod_apply(v: str, t: Optional[XmlStringSerializationType]) -> str: mod = __XS_STRING_MODS.get(t) # type: ignore[arg-type] return mod(v) if mod else v # endregion _xs_string_mod_apply def _allow_property_for_view(prop_info: 'ObjectMetadataLibrary.SerializableProperty', value_: Any, view_: Optional[Type[ViewType]]) -> bool: # First check Property is part of the View is given allow_for_view = False if view_: if prop_info.views and view_ in prop_info.views: allow_for_view = True elif not prop_info.views: allow_for_view = True else: if not prop_info.views: allow_for_view = True # Second check for inclusion of None values if value_ is None or (prop_info.is_array and len(value_) < 1): if not prop_info.include_none: allow_for_view = False elif prop_info.include_none and prop_info.include_none_views: allow_for_view = False for _v, _a in prop_info.include_none_views: if _v == view_: allow_for_view = True return allow_for_view class _SerializableJsonEncoder(JSONEncoder): """ ``py_serializable``'s custom implementation of ``JSONEncode``. You don't need to call this directly - it is all handled for you by ``py_serializable``. """ def __init__(self, *, skipkeys: bool = False, ensure_ascii: bool = True, check_circular: bool = True, allow_nan: bool = True, sort_keys: bool = False, indent: Optional[int] = None, separators: Optional[Tuple[str, str]] = None, default: Optional[Callable[[Any], Any]] = None, view_: Optional[Type[ViewType]] = None) -> None: super().__init__( skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular, allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, separators=separators, default=default ) self._view: Optional[Type[ViewType]] = view_ @property def view(self) -> Optional[Type[ViewType]]: return self._view def default(self, o: Any) -> Any: # Enum if isinstance(o, Enum): return o.value # Iterables if isinstance(o, (list, set)): return list(o) # Classes if isinstance(o, object): d: Dict[Any, Any] = {} klass_qualified_name = f'{o.__module__}.{o.__class__.__qualname__}' serializable_property_info = ObjectMetadataLibrary.klass_property_mappings.get(klass_qualified_name, {}) # Handle remaining Properties that will be sub elements for k, prop_info in serializable_property_info.items(): v = getattr(o, k) if not _allow_property_for_view(prop_info=prop_info, view_=self._view, value_=v): # Skip as rendering for a view and this Property is not registered form this View continue new_key = BaseNameFormatter.decode_handle_python_builtins_and_keywords(name=k) if custom_name := prop_info.custom_names.get(SerializationType.JSON): new_key = str(custom_name) if CurrentFormatter.formatter: new_key = CurrentFormatter.formatter.encode(property_name=new_key) if prop_info.custom_type: if prop_info.is_helper_type(): v = prop_info.custom_type.json_normalize( v, view=self._view, prop_info=prop_info, ctx=o.__class__) else: v = prop_info.custom_type(v) elif prop_info.is_array: if len(v) > 0: v = list(v) else: v = None elif prop_info.is_enum: v = str(v.value) elif not prop_info.is_primitive_type(): if isinstance(v, Decimal): if prop_info.string_format: v = float(f'{v:{prop_info.string_format}}') else: v = float(v) else: global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}' if global_klass_name not in ObjectMetadataLibrary.klass_mappings: if prop_info.string_format: v = f'{v:{prop_info.string_format}}' else: v = str(v) if new_key == '.': return v if _allow_property_for_view(prop_info=prop_info, view_=self._view, value_=v): # We need to recheck as values may have been modified above d.update({new_key: v if v is not None else prop_info.get_none_value_for_view(view_=self._view)}) return d # Fallback to default super().default(o=o) class _JsonSerializable(Protocol): def as_json(self: Any, view_: Optional[Type[ViewType]] = None) -> str: """ Internal method that is injected into Classes that are annotated for serialization and deserialization by ``py_serializable``. """ _logger.debug('Dumping %s to JSON with view: %s...', self, view_) return json_dumps(self, cls=_SerializableJsonEncoder, view_=view_) @classmethod def from_json(cls: Type[_T], data: Dict[str, Any]) -> Optional[_T]: """ Internal method that is injected into Classes that are annotated for serialization and deserialization by ``py_serializable``. """ _logger.debug('Rendering JSON to %s...', cls) klass_qualified_name = f'{cls.__module__}.{cls.__qualname__}' klass = ObjectMetadataLibrary.klass_mappings.get(klass_qualified_name) klass_properties = ObjectMetadataLibrary.klass_property_mappings.get(klass_qualified_name, {}) if klass is None: _logger.warning( '%s is not a known py_serializable class', klass_qualified_name, stacklevel=2) return None if len(klass_properties) == 1: k, only_prop = next(iter(klass_properties.items())) if only_prop.custom_names.get(SerializationType.JSON) == '.': return cls(**{only_prop.name: data}) _data = copy(data) for k, v in data.items(): decoded_k = CurrentFormatter.formatter.decode(property_name=k) if decoded_k in klass.ignore_during_deserialization: _logger.debug('Ignoring %s when deserializing %s.%s', k, cls.__module__, cls.__qualname__) del _data[k] continue new_key = None if decoded_k not in klass_properties: _allowed_custom_names = {decoded_k, k} for p, pi in klass_properties.items(): if pi.custom_names.get(SerializationType.JSON) in _allowed_custom_names: new_key = p else: new_key = decoded_k if new_key is None: _logger.error('Unexpected key %s/%s in data being serialized to %s.%s', k, decoded_k, cls.__module__, cls.__qualname__) raise ValueError( f'Unexpected key {k}/{decoded_k} in data being serialized to {cls.__module__}.{cls.__qualname__}' ) del (_data[k]) _data[new_key] = v for k, v in _data.items(): prop_info = klass_properties.get(k) if not prop_info: raise ValueError(f'No Prop Info for {k} in {cls}') try: if prop_info.custom_type: if prop_info.is_helper_type(): _data[k] = prop_info.custom_type.json_denormalize( v, prop_info=prop_info, ctx=klass) else: _data[k] = prop_info.custom_type(v) elif prop_info.is_array: items = [] for j in v: if not prop_info.is_primitive_type() and not prop_info.is_enum: items.append(prop_info.concrete_type.from_json(data=j)) else: items.append(prop_info.concrete_type(j)) _data[k] = items # type: ignore elif prop_info.is_enum: _data[k] = prop_info.concrete_type(v) elif not prop_info.is_primitive_type(): global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}' if global_klass_name in ObjectMetadataLibrary.klass_mappings: _data[k] = prop_info.concrete_type.from_json(data=v) else: if prop_info.concrete_type is Decimal: v = str(v) _data[k] = prop_info.concrete_type(v) except AttributeError as e: _logger.exception('There was an AttributeError deserializing JSON to %s.\n' 'The Property is: %s\n' 'The Value was: %s\n', cls, prop_info, v) raise AttributeError( f'There was an AttributeError deserializing JSON to {cls} the Property {prop_info}: {e}' ) from e _logger.debug('Creating %s from %s', cls, _data) return cls(**_data) _XML_BOOL_REPRESENTATIONS_TRUE = ('1', 'true') class _XmlSerializable(Protocol): def as_xml(self: Any, view_: Optional[Type[ViewType]] = None, as_string: bool = True, element_name: Optional[str] = None, xmlns: Optional[str] = None) -> Union[Element, str]: """ Internal method that is injected into Classes that are annotated for serialization and deserialization by ``py_serializable``. """ _logger.debug('Dumping %s to XML with view %s...', self, view_) this_e_attributes = {} klass_qualified_name = f'{self.__class__.__module__}.{self.__class__.__qualname__}' serializable_property_info = {k: v for k, v in sorted( ObjectMetadataLibrary.klass_property_mappings.get(klass_qualified_name, {}).items(), key=lambda i: i[1].xml_sequence)} for k, v in self.__dict__.items(): # Remove leading _ in key names new_key = k[1:] if new_key.startswith('_') or '__' in new_key: continue new_key = BaseNameFormatter.decode_handle_python_builtins_and_keywords(name=new_key) if new_key in serializable_property_info: prop_info = cast('ObjectMetadataLibrary.SerializableProperty', serializable_property_info.get(new_key)) if not _allow_property_for_view(prop_info=prop_info, view_=view_, value_=v): # Skip as rendering for a view and this Property is not registered form this View continue if prop_info and prop_info.is_xml_attribute: new_key = prop_info.custom_names.get(SerializationType.XML, new_key) if CurrentFormatter.formatter: new_key = CurrentFormatter.formatter.encode(property_name=new_key) if prop_info.custom_type and prop_info.is_helper_type(): v = prop_info.custom_type.xml_normalize( v, view=view_, element_name=new_key, xmlns=xmlns, prop_info=prop_info, ctx=self.__class__) elif prop_info.is_enum: v = v.value if v is None: v = prop_info.get_none_value_for_view(view_=view_) if v is None: continue this_e_attributes[_namespace_element_name(new_key, xmlns)] = \ _xs_string_mod_apply(str(v), prop_info.xml_string_config) element_name = _namespace_element_name( element_name if element_name else CurrentFormatter.formatter.encode(self.__class__.__name__), xmlns) this_e = Element(element_name, this_e_attributes) # Handle remaining Properties that will be sub elements for k, prop_info in serializable_property_info.items(): # Skip if rendering for a View and this Property is not designated for this View v = getattr(self, k) if not _allow_property_for_view(prop_info=prop_info, view_=view_, value_=v): # Skip as rendering for a view and this Property is not registered form this View continue new_key = BaseNameFormatter.decode_handle_python_builtins_and_keywords(name=k) if not prop_info: raise ValueError(f'{new_key} is not a known Property for {klass_qualified_name}') if not prop_info.is_xml_attribute: new_key = prop_info.custom_names.get(SerializationType.XML, new_key) if v is None: v = prop_info.get_none_value_for_view(view_=view_) if v is None: SubElement(this_e, _namespace_element_name(tag_name=new_key, xmlns=xmlns)) continue if new_key == '.': this_e.text = _xs_string_mod_apply(str(v), prop_info.xml_string_config) continue if CurrentFormatter.formatter: new_key = CurrentFormatter.formatter.encode(property_name=new_key) new_key = _namespace_element_name(new_key, xmlns) if prop_info.is_array and prop_info.xml_array_config: _array_type, nested_key = prop_info.xml_array_config nested_key = _namespace_element_name(nested_key, xmlns) if _array_type and _array_type == XmlArraySerializationType.NESTED: nested_e = SubElement(this_e, new_key) else: nested_e = this_e for j in v: if not prop_info.is_primitive_type() and not prop_info.is_enum: nested_e.append( j.as_xml(view_=view_, as_string=False, element_name=nested_key, xmlns=xmlns)) elif prop_info.is_enum: SubElement(nested_e, nested_key).text = _xs_string_mod_apply(str(j.value), prop_info.xml_string_config) elif prop_info.concrete_type in (float, int): SubElement(nested_e, nested_key).text = str(j) elif prop_info.concrete_type is bool: SubElement(nested_e, nested_key).text = str(j).lower() else: # Assume type is str SubElement(nested_e, nested_key).text = _xs_string_mod_apply(str(j), prop_info.xml_string_config) elif prop_info.custom_type: if prop_info.is_helper_type(): v_ser = prop_info.custom_type.xml_normalize( v, view=view_, element_name=new_key, xmlns=xmlns, prop_info=prop_info, ctx=self.__class__) if v_ser is None: pass # skip the element elif isinstance(v_ser, Element): this_e.append(v_ser) else: SubElement(this_e, new_key).text = _xs_string_mod_apply(str(v_ser), prop_info.xml_string_config) else: SubElement(this_e, new_key).text = _xs_string_mod_apply(str(prop_info.custom_type(v)), prop_info.xml_string_config) elif prop_info.is_enum: SubElement(this_e, new_key).text = _xs_string_mod_apply(str(v.value), prop_info.xml_string_config) elif not prop_info.is_primitive_type(): global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}' if global_klass_name in ObjectMetadataLibrary.klass_mappings: # Handle other Serializable Classes this_e.append(v.as_xml(view_=view_, as_string=False, element_name=new_key, xmlns=xmlns)) else: # Handle properties that have a type that is not a Python Primitive (e.g. int, float, str) if prop_info.string_format: SubElement(this_e, new_key).text = _xs_string_mod_apply(f'{v:{prop_info.string_format}}', prop_info.xml_string_config) else: SubElement(this_e, new_key).text = _xs_string_mod_apply(str(v), prop_info.xml_string_config) elif prop_info.concrete_type in (float, int): SubElement(this_e, new_key).text = str(v) elif prop_info.concrete_type is bool: SubElement(this_e, new_key).text = str(v).lower() else: # Assume type is str SubElement(this_e, new_key).text = _xs_string_mod_apply(str(v), prop_info.xml_string_config) if as_string: return cast(Element, SafeElementTree.tostring(this_e, 'unicode')) else: return this_e @classmethod def from_xml(cls: Type[_T], data: Union[TextIOBase, Element], default_namespace: Optional[str] = None) -> Optional[_T]: """ Internal method that is injected into Classes that are annotated for serialization and deserialization by ``py_serializable``. """ _logger.debug('Rendering XML from %s to %s...', type(data), cls) klass = ObjectMetadataLibrary.klass_mappings.get(f'{cls.__module__}.{cls.__qualname__}') if klass is None: _logger.warning('%s.%s is not a known py_serializable class', cls.__module__, cls.__qualname__, stacklevel=2) return None klass_properties = ObjectMetadataLibrary.klass_property_mappings.get(f'{cls.__module__}.{cls.__qualname__}', {}) if isinstance(data, TextIOBase): data = cast(Element, SafeElementTree.fromstring(data.read())) if default_namespace is None: _namespaces = dict(node for _, node in SafeElementTree.iterparse(StringIO(SafeElementTree.tostring(data, 'unicode')), events=['start-ns'])) default_namespace = (re_compile(r'^\{(.*?)\}.').search(data.tag) or (None, _namespaces.get('')))[1] if default_namespace is None: def strip_default_namespace(s: str) -> str: return s else: def strip_default_namespace(s: str) -> str: return s.replace(f'{{{default_namespace}}}', '') _data: Dict[str, Any] = {} # Handle attributes on the root element if there are any for k, v in data.attrib.items(): decoded_k = CurrentFormatter.formatter.decode(strip_default_namespace(k)) if decoded_k in klass.ignore_during_deserialization: _logger.debug('Ignoring %s when deserializing %s.%s', decoded_k, cls.__module__, cls.__qualname__) continue if decoded_k not in klass_properties: for p, pi in klass_properties.items(): if pi.custom_names.get(SerializationType.XML) == decoded_k: decoded_k = p prop_info = klass_properties.get(decoded_k) if not prop_info: raise ValueError(f'Non-primitive types not supported from XML Attributes - see {decoded_k} for ' f'{cls.__module__}.{cls.__qualname__} which has Prop Metadata: {prop_info}') if prop_info.xml_string_config: v = _xs_string_mod_apply(v, prop_info.xml_string_config) if prop_info.custom_type and prop_info.is_helper_type(): _data[decoded_k] = prop_info.custom_type.xml_deserialize(v) elif prop_info.is_enum: _data[decoded_k] = prop_info.concrete_type(v) elif prop_info.is_primitive_type(): _data[decoded_k] = prop_info.concrete_type(v) else: raise ValueError(f'Non-primitive types not supported from XML Attributes - see {decoded_k}') # Handle Node text content if data.text: for p, pi in klass_properties.items(): if pi.custom_names.get(SerializationType.XML) == '.': _data[p] = _xs_string_mod_apply(data.text.strip(), pi.xml_string_config) # Handle Sub-Elements for child_e in data: decoded_k = CurrentFormatter.formatter.decode(strip_default_namespace(child_e.tag)) if decoded_k not in klass_properties: for p, pi in klass_properties.items(): if pi.xml_array_config: array_type, nested_name = pi.xml_array_config if nested_name == strip_default_namespace(child_e.tag): decoded_k = p if decoded_k in klass.ignore_during_deserialization: _logger.debug('Ignoring %s when deserializing %s.%s', decoded_k, cls.__module__, cls.__qualname__) continue if decoded_k not in klass_properties: for p, pi in klass_properties.items(): if pi.xml_array_config: array_type, nested_name = pi.xml_array_config if nested_name == decoded_k: if array_type == XmlArraySerializationType.FLAT: decoded_k = p else: decoded_k = '____SKIP_ME____' elif pi.custom_names.get(SerializationType.XML) == decoded_k: decoded_k = p if decoded_k == '____SKIP_ME____': continue prop_info = klass_properties.get(decoded_k) if not prop_info: raise ValueError(f'{decoded_k} is not a known Property for {cls.__module__}.{cls.__qualname__}') try: _logger.debug('Handling %s', prop_info) if child_e.text: child_e.text = _xs_string_mod_apply(child_e.text, prop_info.xml_string_config) if prop_info.is_array and prop_info.xml_array_config: array_type, nested_name = prop_info.xml_array_config if decoded_k not in _data: _data[decoded_k] = [] if array_type == XmlArraySerializationType.NESTED: for sub_child_e in child_e: if sub_child_e.text: sub_child_e.text = _xs_string_mod_apply(sub_child_e.text, prop_info.xml_string_config) if not prop_info.is_primitive_type() and not prop_info.is_enum: _data[decoded_k].append(prop_info.concrete_type.from_xml( data=sub_child_e, default_namespace=default_namespace) ) else: _data[decoded_k].append(prop_info.concrete_type(sub_child_e.text)) else: if not prop_info.is_primitive_type() and not prop_info.is_enum: _data[decoded_k].append(prop_info.concrete_type.from_xml( data=child_e, default_namespace=default_namespace) ) elif prop_info.custom_type: if prop_info.is_helper_type(): _data[decoded_k] = prop_info.custom_type.xml_denormalize( child_e, default_ns=default_namespace, prop_info=prop_info, ctx=klass) else: _data[decoded_k] = prop_info.custom_type(child_e.text) else: _data[decoded_k].append(prop_info.concrete_type(child_e.text)) elif prop_info.custom_type: if prop_info.is_helper_type(): _data[decoded_k] = prop_info.custom_type.xml_denormalize( child_e, default_ns=default_namespace, prop_info=prop_info, ctx=klass) else: _data[decoded_k] = prop_info.custom_type(child_e.text) elif prop_info.is_enum: _data[decoded_k] = prop_info.concrete_type(child_e.text) elif not prop_info.is_primitive_type(): global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}' if global_klass_name in ObjectMetadataLibrary.klass_mappings: _data[decoded_k] = prop_info.concrete_type.from_xml( data=child_e, default_namespace=default_namespace ) else: _data[decoded_k] = prop_info.concrete_type(child_e.text) else: if prop_info.concrete_type == bool: _data[decoded_k] = str(child_e.text) in _XML_BOOL_REPRESENTATIONS_TRUE else: _data[decoded_k] = prop_info.concrete_type(child_e.text) except AttributeError as e: _logger.exception('There was an AttributeError deserializing JSON to %s.\n' 'The Property is: %s\n' 'The Value was: %s\n', cls, prop_info, v) raise AttributeError('There was an AttributeError deserializing XML ' f'to {cls} the Property {prop_info}: {e}') from e _logger.debug('Creating %s from %s', cls, _data) if len(_data) == 0: return None return cls(**_data) def _namespace_element_name(tag_name: str, xmlns: Optional[str]) -> str: if tag_name.startswith('{'): return tag_name if xmlns: return f'{{{xmlns}}}{tag_name}' return tag_name class ObjectMetadataLibrary: """namespace-like The core Class in ``py_serializable`` that is used to record all metadata about classes that you annotate for serialization and deserialization. """ _deferred_property_type_parsing: Dict[str, Set['ObjectMetadataLibrary.SerializableProperty']] = {} _klass_views: Dict[str, Type[ViewType]] = {} _klass_property_array_config: Dict[str, Tuple[XmlArraySerializationType, str]] = {} _klass_property_string_config: Dict[str, Optional[XmlStringSerializationType]] = {} _klass_property_attributes: Set[str] = set() _klass_property_include_none: Dict[str, Set[Tuple[Type[ViewType], Any]]] = {} _klass_property_names: Dict[str, Dict[SerializationType, str]] = {} _klass_property_string_formats: Dict[str, str] = {} _klass_property_types: Dict[str, type] = {} _klass_property_views: Dict[str, Set[Type[ViewType]]] = {} _klass_property_xml_sequence: Dict[str, int] = {} custom_enum_klasses: Set[Type[Enum]] = set() klass_mappings: Dict[str, 'ObjectMetadataLibrary.SerializableClass'] = {} klass_property_mappings: Dict[str, Dict[str, 'ObjectMetadataLibrary.SerializableProperty']] = {} class SerializableClass: """ Internal model class used to represent metadata we hold about Classes that are being included in (de-)serialization. """ def __init__(self, *, klass: type, custom_name: Optional[str] = None, serialization_types: Optional[Iterable[SerializationType]] = None, ignore_during_deserialization: Optional[Iterable[str]] = None) -> None: self._name = str(klass.__name__) self._klass = klass self._custom_name = custom_name if serialization_types is None: serialization_types = _DEFAULT_SERIALIZATION_TYPES self._serialization_types = serialization_types self._ignore_during_deserialization = set(ignore_during_deserialization or ()) @property def name(self) -> str: return self._name @property def klass(self) -> type: return self._klass @property def custom_name(self) -> Optional[str]: return self._custom_name @property def serialization_types(self) -> Iterable[SerializationType]: return self._serialization_types @property def ignore_during_deserialization(self) -> Set[str]: return self._ignore_during_deserialization def __repr__(self) -> str: return f'' class SerializableProperty: """ Internal model class used to represent metadata we hold about Properties that are being included in (de-)serialization. """ _ARRAY_TYPES = {'List': List, 'Set': Set, 'SortedSet': Set} _SORTED_CONTAINERS_TYPES = {'SortedList': List, 'SortedSet': Set} _PRIMITIVE_TYPES = (bool, int, float, str) _DEFAULT_XML_SEQUENCE = 100 def __init__(self, *, prop_name: str, prop_type: Any, custom_names: Dict[SerializationType, str], custom_type: Optional[Any] = None, include_none_config: Optional[Set[Tuple[Type[ViewType], Any]]] = None, is_xml_attribute: bool = False, string_format_: Optional[str] = None, views: Optional[Iterable[Type[ViewType]]] = None, xml_array_config: Optional[Tuple[XmlArraySerializationType, str]] = None, xml_string_config: Optional[XmlStringSerializationType] = None, xml_sequence_: Optional[int] = None) -> None: self._name = prop_name self._custom_names = custom_names self._type_ = None self._concrete_type = None self._is_array = False self._is_enum = False self._is_optional = False self._custom_type = custom_type if include_none_config is not None: self._include_none = True self._include_none_views = include_none_config else: self._include_none = False self._include_none_views = set() self._is_xml_attribute = is_xml_attribute self._string_format = string_format_ self._views = set(views or ()) self._xml_array_config = xml_array_config self._xml_string_config = xml_string_config self._xml_sequence = xml_sequence_ or self._DEFAULT_XML_SEQUENCE self._deferred_type_parsing = False self._parse_type(type_=prop_type) @property def name(self) -> str: return self._name @property def custom_names(self) -> Dict[SerializationType, str]: return self._custom_names def custom_name(self, serialization_type: SerializationType) -> Optional[str]: return self.custom_names.get(serialization_type) @property def type_(self) -> Any: return self._type_ @property def concrete_type(self) -> Any: return self._concrete_type @property def custom_type(self) -> Optional[Any]: return self._custom_type @property def include_none(self) -> bool: return self._include_none @property def include_none_views(self) -> Set[Tuple[Type[ViewType], Any]]: return self._include_none_views def include_none_for_view(self, view_: Type[ViewType]) -> bool: for _v, _a in self._include_none_views: if _v == view_: return True return False def get_none_value_for_view(self, view_: Optional[Type[ViewType]]) -> Any: if view_: for _v, _a in self._include_none_views: if _v == view_: return _a return None @property def is_xml_attribute(self) -> bool: return self._is_xml_attribute @property def string_format(self) -> Optional[str]: return self._string_format @property def views(self) -> Set[Type[ViewType]]: return self._views @property def xml_array_config(self) -> Optional[Tuple[XmlArraySerializationType, str]]: return self._xml_array_config @property def is_array(self) -> bool: return self._is_array @property def xml_string_config(self) -> Optional[XmlStringSerializationType]: return self._xml_string_config @property def is_enum(self) -> bool: return self._is_enum @property def is_optional(self) -> bool: return self._is_optional @property def xml_sequence(self) -> int: return self._xml_sequence def get_none_value(self, view_: Optional[Type[ViewType]] = None) -> Any: if not self.include_none: raise ValueError('No None Value for property that is not include_none') def is_helper_type(self) -> bool: ct = self.custom_type return isclass(ct) and issubclass(ct, BaseHelper) def is_primitive_type(self) -> bool: return self.concrete_type in self._PRIMITIVE_TYPES def parse_type_deferred(self) -> None: self._parse_type(type_=self._type_) def _parse_type(self, type_: Any) -> None: self._type_ = type_ = self._handle_forward_ref(t_=type_) if type(type_) is str: type_to_parse = str(type_) # Handle types that are quoted strings e.g. 'SortedSet[MyObject]' or 'Optional[SortedSet[MyObject]]' if type_to_parse.startswith('typing.Optional['): self._is_optional = True type_to_parse = type_to_parse[16:-1] elif type_to_parse.startswith('Optional['): self._is_optional = True type_to_parse = type_to_parse[9:-1] match = re_search(r"^(?P[\w.]+)\[['\"]?(?P\w+)['\"]?]$", type_to_parse) if match: results = match.groupdict() if results.get('array_type') in self._SORTED_CONTAINERS_TYPES: mapped_array_type = self._SORTED_CONTAINERS_TYPES.get(str(results.get('array_type'))) self._is_array = True try: # Will load any class already loaded assuming fully qualified name self._type_ = eval(f'{mapped_array_type}[{results.get("array_of")}]') self._concrete_type = eval(str(results.get('array_of'))) except NameError: # Likely a class that is missing its fully qualified name _k: Optional[Any] = None for _k_name, _oml_sc in ObjectMetadataLibrary.klass_mappings.items(): if _oml_sc.name == results.get('array_of'): _k = _oml_sc.klass if _k is None: # Perhaps a custom ENUM? for _enum_klass in ObjectMetadataLibrary.custom_enum_klasses: if _enum_klass.__name__ == results.get('array_of'): _k = _enum_klass if _k is None: self._type_ = type_ # type: ignore self._deferred_type_parsing = True ObjectMetadataLibrary.defer_property_type_parsing( prop=self, klasses=[str(results.get('array_of'))] ) return self._type_ = mapped_array_type[_k] # type: ignore self._concrete_type = _k # type: ignore elif results.get('array_type', '').replace('typing.', '') in self._ARRAY_TYPES: mapped_array_type = self._ARRAY_TYPES.get( str(results.get('array_type', '').replace('typing.', '')) ) self._is_array = True try: # Will load any class already loaded assuming fully qualified name self._type_ = eval(f'{mapped_array_type}[{results.get("array_of")}]') self._concrete_type = eval(str(results.get('array_of'))) except NameError: # Likely a class that is missing its fully qualified name _l: Optional[Any] = None for _k_name, _oml_sc in ObjectMetadataLibrary.klass_mappings.items(): if _oml_sc.name == results.get('array_of'): _l = _oml_sc.klass if _l is None: # Perhaps a custom ENUM? for _enum_klass in ObjectMetadataLibrary.custom_enum_klasses: if _enum_klass.__name__ == results.get('array_of'): _l = _enum_klass if _l is None: self._type_ = type_ # type: ignore self._deferred_type_parsing = True ObjectMetadataLibrary.defer_property_type_parsing( prop=self, klasses=[str(results.get('array_of'))] ) return self._type_ = mapped_array_type[_l] # type: ignore self._concrete_type = _l # type: ignore else: raise ValueError(f'Unable to handle Property with declared type: {type_}') else: # Handle real types if len(getattr(self.type_, '__args__', ())) > 1: # Is this an Optional Property self._is_optional = type(None) in self.type_.__args__ if self.is_optional: t, n = self.type_.__args__ if getattr(t, '_name', None) in self._ARRAY_TYPES: self._is_array = True t, = t.__args__ self._concrete_type = t else: if getattr(self.type_, '_name', None) in self._ARRAY_TYPES: self._is_array = True self._concrete_type, = self.type_.__args__ else: self._concrete_type = self.type_ # Handle Enums if issubclass(type(self.concrete_type), EnumMeta): self._is_enum = True # Ensure marked as not deferred if self._deferred_type_parsing: self._deferred_type_parsing = False def _handle_forward_ref(self, t_: Any) -> Any: if 'ForwardRef' in str(t_): return str(t_).replace("ForwardRef('", '"').replace("')", '"') else: return t_ def __eq__(self, other: Any) -> bool: if isinstance(other, ObjectMetadataLibrary.SerializableProperty): return hash(other) == hash(self) return False def __lt__(self, other: Any) -> bool: if isinstance(other, ObjectMetadataLibrary.SerializableProperty): return self.xml_sequence < other.xml_sequence return NotImplemented def __hash__(self) -> int: return hash(( self.concrete_type, tuple(self.custom_names), self.custom_type, self.is_array, self.is_enum, self.is_optional, self.is_xml_attribute, self.name, self.type_, tuple(self.xml_array_config) if self.xml_array_config else None, self.xml_sequence )) def __repr__(self) -> str: return f'' @classmethod def defer_property_type_parsing(cls, prop: 'ObjectMetadataLibrary.SerializableProperty', klasses: Iterable[str]) -> None: for _k in klasses: if _k not in ObjectMetadataLibrary._deferred_property_type_parsing: ObjectMetadataLibrary._deferred_property_type_parsing[_k] = set() ObjectMetadataLibrary._deferred_property_type_parsing[_k].add(prop) @classmethod def is_klass_serializable(cls, klass: Any) -> bool: if type(klass) is Type: return f'{klass.__module__}.{klass.__name__}' in cls.klass_mappings # type: ignore return klass in cls.klass_mappings @classmethod def is_property(cls, o: Any) -> bool: return isinstance(o, property) @classmethod def register_enum(cls, klass: Type[_E]) -> Type[_E]: cls.custom_enum_klasses.add(klass) return klass @classmethod def register_klass(cls, klass: Type[_T], custom_name: Optional[str], serialization_types: Iterable[SerializationType], ignore_during_deserialization: Optional[Iterable[str]] = None ) -> Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]]: if cls.is_klass_serializable(klass=klass): return klass cls.klass_mappings[f'{klass.__module__}.{klass.__qualname__}'] = ObjectMetadataLibrary.SerializableClass( klass=klass, serialization_types=serialization_types, ignore_during_deserialization=ignore_during_deserialization ) qualified_class_name = f'{klass.__module__}.{klass.__qualname__}' cls.klass_property_mappings[qualified_class_name] = {} _logger.debug('Registering Class %s with custom name %s', qualified_class_name, custom_name) for name, o in getmembers(klass, ObjectMetadataLibrary.is_property): qualified_property_name = f'{qualified_class_name}.{name}' prop_arg_specs = getfullargspec(o.fget) cls.klass_property_mappings[qualified_class_name][name] = ObjectMetadataLibrary.SerializableProperty( prop_name=name, custom_names=ObjectMetadataLibrary._klass_property_names.get(qualified_property_name, {}), prop_type=prop_arg_specs.annotations.get('return'), custom_type=ObjectMetadataLibrary._klass_property_types.get(qualified_property_name), include_none_config=ObjectMetadataLibrary._klass_property_include_none.get(qualified_property_name), is_xml_attribute=(qualified_property_name in ObjectMetadataLibrary._klass_property_attributes), string_format_=ObjectMetadataLibrary._klass_property_string_formats.get(qualified_property_name), views=ObjectMetadataLibrary._klass_property_views.get(qualified_property_name), xml_array_config=ObjectMetadataLibrary._klass_property_array_config.get(qualified_property_name), xml_string_config=ObjectMetadataLibrary._klass_property_string_config.get(qualified_property_name), xml_sequence_=ObjectMetadataLibrary._klass_property_xml_sequence.get( qualified_property_name, ObjectMetadataLibrary.SerializableProperty._DEFAULT_XML_SEQUENCE) ) if SerializationType.JSON in serialization_types: klass.as_json = _JsonSerializable.as_json # type:ignore[attr-defined] klass.from_json = classmethod(_JsonSerializable.from_json.__func__) # type:ignore[attr-defined] if SerializationType.XML in serialization_types: klass.as_xml = _XmlSerializable.as_xml # type:ignore[attr-defined] klass.from_xml = classmethod(_XmlSerializable.from_xml.__func__) # type:ignore[attr-defined] # Handle any deferred Properties depending on this class for _p in ObjectMetadataLibrary._deferred_property_type_parsing.get(klass.__qualname__, ()): _p.parse_type_deferred() return klass @classmethod def register_custom_json_property_name(cls, qual_name: str, json_property_name: str) -> None: prop = cls._klass_property_names.get(qual_name) if prop is None: cls._klass_property_names[qual_name] = {SerializationType.JSON: json_property_name} else: prop[SerializationType.JSON] = json_property_name @classmethod def register_custom_string_format(cls, qual_name: str, string_format: str) -> None: cls._klass_property_string_formats[qual_name] = string_format @classmethod def register_custom_xml_property_name(cls, qual_name: str, xml_property_name: str) -> None: prop = cls._klass_property_names.get(qual_name) if prop: prop[SerializationType.XML] = xml_property_name else: cls._klass_property_names[qual_name] = {SerializationType.XML: xml_property_name} @classmethod def register_klass_view(cls, klass: Type[_T], view_: Type[ViewType]) -> Type[_T]: ObjectMetadataLibrary._klass_views[f'{klass.__module__}.{klass.__qualname__}'] = view_ return klass @classmethod def register_property_include_none(cls, qual_name: str, view_: Optional[Type[ViewType]] = None, none_value: Optional[Any] = None) -> None: prop = cls._klass_property_include_none.get(qual_name) val = (view_ or ViewType, none_value) if prop is None: cls._klass_property_include_none[qual_name] = {val} else: prop.add(val) @classmethod def register_property_view(cls, qual_name: str, view_: Type[ViewType]) -> None: prop = ObjectMetadataLibrary._klass_property_views.get(qual_name) if prop is None: ObjectMetadataLibrary._klass_property_views[qual_name] = {view_} else: prop.add(view_) @classmethod def register_xml_property_array_config(cls, qual_name: str, array_type: XmlArraySerializationType, child_name: str) -> None: cls._klass_property_array_config[qual_name] = (array_type, child_name) @classmethod def register_xml_property_string_config(cls, qual_name: str, string_type: Optional[XmlStringSerializationType]) -> None: cls._klass_property_string_config[qual_name] = string_type @classmethod def register_xml_property_attribute(cls, qual_name: str) -> None: cls._klass_property_attributes.add(qual_name) @classmethod def register_xml_property_sequence(cls, qual_name: str, sequence: int) -> None: cls._klass_property_xml_sequence[qual_name] = sequence @classmethod def register_property_type_mapping(cls, qual_name: str, mapped_type: type) -> None: cls._klass_property_types[qual_name] = mapped_type @overload def serializable_enum(cls: Literal[None] = None) -> Callable[[Type[_E]], Type[_E]]: ... @overload def serializable_enum(cls: Type[_E]) -> Type[_E]: # type:ignore[misc] # mypy on py37 ... def serializable_enum(cls: Optional[Type[_E]] = None) -> Union[ Callable[[Type[_E]], Type[_E]], Type[_E] ]: """Decorator""" def decorate(kls: Type[_E]) -> Type[_E]: ObjectMetadataLibrary.register_enum(klass=kls) return kls # See if we're being called as @enum or @enum(). if cls is None: # We're called with parens. return decorate # We're called as @register_klass without parens. return decorate(cls) @overload def serializable_class( cls: Literal[None] = None, *, name: Optional[str] = ..., serialization_types: Optional[Iterable[SerializationType]] = ..., ignore_during_deserialization: Optional[Iterable[str]] = ... ) -> Callable[[Type[_T]], Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]]]: ... @overload def serializable_class( # type:ignore[misc] # mypy on py37 cls: Type[_T], *, name: Optional[str] = ..., serialization_types: Optional[Iterable[SerializationType]] = ..., ignore_during_deserialization: Optional[Iterable[str]] = ... ) -> Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]]: ... def serializable_class( cls: Optional[Type[_T]] = None, *, name: Optional[str] = None, serialization_types: Optional[Iterable[SerializationType]] = None, ignore_during_deserialization: Optional[Iterable[str]] = None ) -> Union[ Callable[[Type[_T]], Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]]], Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]] ]: """ Decorator used to tell ``py_serializable`` that a class is to be included in (de-)serialization. :param cls: Class :param name: Alternative name to use for this Class :param serialization_types: Serialization Types that are to be supported for this class. :param ignore_during_deserialization: List of properties/elements to ignore during deserialization :return: """ if serialization_types is None: serialization_types = _DEFAULT_SERIALIZATION_TYPES def decorate(kls: Type[_T]) -> Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]]: ObjectMetadataLibrary.register_klass( klass=kls, custom_name=name, serialization_types=serialization_types or [], ignore_during_deserialization=ignore_during_deserialization ) return kls # See if we're being called as @register_klass or @register_klass(). if cls is None: # We're called with parens. return decorate # We're called as @register_klass without parens. return decorate(cls) def type_mapping(type_: type) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: _logger.debug('Registering %s.%s with custom type: %s', f.__module__, f.__qualname__, type_) ObjectMetadataLibrary.register_property_type_mapping( qual_name=f'{f.__module__}.{f.__qualname__}', mapped_type=type_ ) return f return decorate def include_none(view_: Optional[Type[ViewType]] = None, none_value: Optional[Any] = None) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: _logger.debug('Registering %s.%s to include None for view: %s', f.__module__, f.__qualname__, view_) ObjectMetadataLibrary.register_property_include_none( qual_name=f'{f.__module__}.{f.__qualname__}', view_=view_, none_value=none_value ) return f return decorate def json_name(name: str) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: _logger.debug('Registering %s.%s with JSON name: %s', f.__module__, f.__qualname__, name) ObjectMetadataLibrary.register_custom_json_property_name( qual_name=f'{f.__module__}.{f.__qualname__}', json_property_name=name ) return f return decorate def string_format(format_: str) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: _logger.debug('Registering %s.%s with String Format: %s', f.__module__, f.__qualname__, format_) ObjectMetadataLibrary.register_custom_string_format( qual_name=f'{f.__module__}.{f.__qualname__}', string_format=format_ ) return f return decorate def view(view_: Type[ViewType]) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: _logger.debug('Registering %s.%s with View: %s', f.__module__, f.__qualname__, view_) ObjectMetadataLibrary.register_property_view( qual_name=f'{f.__module__}.{f.__qualname__}', view_=view_ ) return f return decorate def xml_attribute() -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: _logger.debug('Registering %s.%s as XML attribute', f.__module__, f.__qualname__) ObjectMetadataLibrary.register_xml_property_attribute(qual_name=f'{f.__module__}.{f.__qualname__}') return f return decorate def xml_array(array_type: XmlArraySerializationType, child_name: str) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: _logger.debug('Registering %s.%s as XML Array: %s:%s', f.__module__, f.__qualname__, array_type, child_name) ObjectMetadataLibrary.register_xml_property_array_config( qual_name=f'{f.__module__}.{f.__qualname__}', array_type=array_type, child_name=child_name ) return f return decorate def xml_string(string_type: XmlStringSerializationType) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: _logger.debug('Registering %s.%s as XML StringType: %s', f.__module__, f.__qualname__, string_type) ObjectMetadataLibrary.register_xml_property_string_config( qual_name=f'{f.__module__}.{f.__qualname__}', string_type=string_type ) return f return decorate def xml_name(name: str) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: _logger.debug('Registering %s.%s with XML name: %s', f.__module__, f.__qualname__, name) ObjectMetadataLibrary.register_custom_xml_property_name( qual_name=f'{f.__module__}.{f.__qualname__}', xml_property_name=name ) return f return decorate def xml_sequence(sequence: int) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: _logger.debug('Registering %s.%s with XML sequence: %s', f.__module__, f.__qualname__, sequence) ObjectMetadataLibrary.register_xml_property_sequence( qual_name=f'{f.__module__}.{f.__qualname__}', sequence=sequence ) return f return decorate serializable-2.0.0/py_serializable/formatters.py000066400000000000000000000065211475213023200221000ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. from abc import ABC, abstractmethod from re import compile as re_compile from typing import Type class BaseNameFormatter(ABC): @classmethod @abstractmethod def encode(cls, property_name: str) -> str: pass @classmethod @abstractmethod def decode(cls, property_name: str) -> str: pass @classmethod def decode_as_class_name(cls, name: str) -> str: name = CamelCasePropertyNameFormatter.encode(cls.decode(property_name=name)) return name[:1].upper() + name[1:] @classmethod def decode_handle_python_builtins_and_keywords(cls, name: str) -> str: return name @classmethod def encode_handle_python_builtins_and_keywords(cls, name: str) -> str: return name class CamelCasePropertyNameFormatter(BaseNameFormatter): _ENCODE_PATTERN = re_compile(r'_([a-z])') _DECODE_PATTERN = re_compile(r'(? str: property_name = property_name[:1].lower() + property_name[1:] return cls.encode_handle_python_builtins_and_keywords( CamelCasePropertyNameFormatter._ENCODE_PATTERN.sub(lambda x: x.group(1).upper(), property_name) ) @classmethod def decode(cls, property_name: str) -> str: return cls.decode_handle_python_builtins_and_keywords( CamelCasePropertyNameFormatter._DECODE_PATTERN.sub('_', property_name).lower() ) class KebabCasePropertyNameFormatter(BaseNameFormatter): _ENCODE_PATTERN = re_compile(r'(_)') @classmethod def encode(cls, property_name: str) -> str: property_name = cls.encode_handle_python_builtins_and_keywords(name=property_name) property_name = property_name[:1].lower() + property_name[1:] return KebabCasePropertyNameFormatter._ENCODE_PATTERN.sub(lambda x: '-', property_name) @classmethod def decode(cls, property_name: str) -> str: return cls.decode_handle_python_builtins_and_keywords(property_name.replace('-', '_')) class SnakeCasePropertyNameFormatter(BaseNameFormatter): _ENCODE_PATTERN = re_compile(r'(.)([A-Z][a-z]+)') @classmethod def encode(cls, property_name: str) -> str: property_name = property_name[:1].lower() + property_name[1:] return cls.encode_handle_python_builtins_and_keywords( SnakeCasePropertyNameFormatter._ENCODE_PATTERN.sub(lambda x: x.group(1).upper(), property_name) ) @classmethod def decode(cls, property_name: str) -> str: return cls.decode_handle_python_builtins_and_keywords(property_name) class CurrentFormatter: formatter: Type['BaseNameFormatter'] = CamelCasePropertyNameFormatter serializable-2.0.0/py_serializable/helpers.py000066400000000000000000000163741475213023200213630ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. from datetime import date, datetime from logging import getLogger from re import compile as re_compile from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, Union if TYPE_CHECKING: # pragma: no cover from xml.etree.ElementTree import Element from . import ObjectMetadataLibrary, ViewType _T = TypeVar('_T') _logger = getLogger(__name__) class BaseHelper: """Base Helper. Inherit from this class and implement/override the needed functions! This class does not provide any functionality, it is more like a Protocol with some fallback implementations. """ # region general/fallback @classmethod def serialize(cls, o: Any) -> Union[Any, str]: """general purpose serializer""" raise NotImplementedError() @classmethod def deserialize(cls, o: Any) -> Any: """general purpose deserializer""" raise NotImplementedError() # endregion general/fallback # region json specific @classmethod def json_normalize(cls, o: Any, *, view: Optional[Type['ViewType']], prop_info: 'ObjectMetadataLibrary.SerializableProperty', ctx: Type[Any], **kwargs: Any) -> Optional[Any]: """json specific normalizer""" return cls.json_serialize(o) @classmethod def json_serialize(cls, o: Any) -> Union[str, Any]: """json specific serializer""" return cls.serialize(o) @classmethod def json_denormalize(cls, o: Any, *, prop_info: 'ObjectMetadataLibrary.SerializableProperty', ctx: Type[Any], **kwargs: Any) -> Any: """json specific denormalizer :param tCls: the class that was desired to denormalize to :param pCls: tha prent class - as context """ return cls.json_deserialize(o) @classmethod def json_deserialize(cls, o: Any) -> Any: """json specific deserializer""" return cls.deserialize(o) # endregion json specific # region xml specific @classmethod def xml_normalize(cls, o: Any, *, element_name: str, view: Optional[Type['ViewType']], xmlns: Optional[str], prop_info: 'ObjectMetadataLibrary.SerializableProperty', ctx: Type[Any], **kwargs: Any) -> Optional[Union['Element', Any]]: """xml specific normalizer""" return cls.xml_serialize(o) @classmethod def xml_serialize(cls, o: Any) -> Union[str, Any]: """xml specific serializer""" return cls.serialize(o) @classmethod def xml_denormalize(cls, o: 'Element', *, default_ns: Optional[str], prop_info: 'ObjectMetadataLibrary.SerializableProperty', ctx: Type[Any], **kwargs: Any) -> Any: """xml specific denormalizer""" return cls.xml_deserialize(o.text) @classmethod def xml_deserialize(cls, o: Union[str, Any]) -> Any: """xml specific deserializer""" return cls.deserialize(o) # endregion xml specific class Iso8601Date(BaseHelper): _PATTERN_DATE = '%Y-%m-%d' @classmethod def serialize(cls, o: Any) -> str: if isinstance(o, date): return o.strftime(Iso8601Date._PATTERN_DATE) raise ValueError(f'Attempt to serialize a non-date: {o.__class__}') @classmethod def deserialize(cls, o: Any) -> date: try: return date.fromisoformat(str(o)) except ValueError: raise ValueError(f'Date string supplied ({o}) does not match either "{Iso8601Date._PATTERN_DATE}"') class XsdDate(BaseHelper): @classmethod def serialize(cls, o: Any) -> str: if isinstance(o, date): return o.isoformat() raise ValueError(f'Attempt to serialize a non-date: {o.__class__}') @classmethod def deserialize(cls, o: Any) -> date: try: v = str(o) if v.startswith('-'): # Remove any leading hyphen v = v[1:] if v.endswith('Z'): v = v[:-1] _logger.warning( 'Potential data loss will occur: dates with timezones not supported in Python', stacklevel=2) if '+' in v: v = v[:v.index('+')] _logger.warning( 'Potential data loss will occur: dates with timezones not supported in Python', stacklevel=2) return date.fromisoformat(v) except ValueError: raise ValueError(f'Date string supplied ({o}) is not a supported ISO Format') class XsdDateTime(BaseHelper): @staticmethod def __fix_tz(dt: datetime) -> datetime: """ Fix for Python's violation of ISO8601: :py:meth:`datetime.isoformat()` might omit the time offset when in doubt, but the ISO-8601 assumes local time zone. Anyway, the time offset is mandatory for this purpose. """ return dt.astimezone() \ if dt.tzinfo is None \ else dt @classmethod def serialize(cls, o: Any) -> str: if isinstance(o, datetime): return cls.__fix_tz(o).isoformat() raise ValueError(f'Attempt to serialize a non-date: {o.__class__}') # region fixup_microseconds # see https://github.com/madpah/serializable/pull/138 __PATTERN_FRACTION = re_compile(r'\.\d+') @classmethod def __fix_microseconds(cls, v: str) -> str: """ Fix for Python's violation of ISO8601 for :py:meth:`datetime.fromisoformat`. 1. Ensure either 0 or exactly 6 decimal places for seconds. Background: py<3.11 supports either 6 or 0 digits for milliseconds when parsing. 2. Ensure correct rounding of microseconds on the 6th digit. """ return cls.__PATTERN_FRACTION.sub(lambda m: f'{(float(m.group(0))):.6f}'[1:], v) # endregion fixup_microseconds @classmethod def deserialize(cls, o: Any) -> datetime: try: v = str(o) if v.startswith('-'): # Remove any leading hyphen v = v[1:] if v.endswith('Z'): # Replace ZULU time with 00:00 offset v = f'{v[:-1]}+00:00' return datetime.fromisoformat( cls.__fix_microseconds(v)) except ValueError: raise ValueError(f'Date-Time string supplied ({o}) is not a supported ISO Format') serializable-2.0.0/py_serializable/json.py000066400000000000000000000013341475213023200206600ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. """ JSON-specific functionality. """ serializable-2.0.0/py_serializable/py.typed000066400000000000000000000002311475213023200210270ustar00rootroot00000000000000# Marker file for PEP 561. This package uses inline types. # This file is needed to allow other packages to type-check their code against this package. serializable-2.0.0/py_serializable/xml.py000066400000000000000000000062541475213023200205150ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. """ XML-specific functionality. """ __all__ = ['xs_normalizedString', 'xs_token'] from re import compile as re_compile # region normalizedString __NORMALIZED_STRING_FORBIDDEN_SEARCH = re_compile(r'\r\n|\t|\n|\r') __NORMALIZED_STRING_FORBIDDEN_REPLACE = ' ' def xs_normalizedString(s: str) -> str: """Make a ``normalizedString``, adhering XML spec. .. epigraph:: *normalizedString* represents white space normalized strings. The `·value space· `_ of normalizedString is the set of strings that do not contain the carriage return (#xD), line feed (#xA) nor tab (#x9) characters. The `·lexical space· `_ of normalizedString is the set of strings that do not contain the carriage return (#xD), line feed (#xA) nor tab (#x9) characters. The `·base type· `_ of normalizedString is `string `_. -- the `XML schema spec `_ """ return __NORMALIZED_STRING_FORBIDDEN_SEARCH.sub( __NORMALIZED_STRING_FORBIDDEN_REPLACE, s) # endregion # region token __TOKEN_MULTISTRING_SEARCH = re_compile(r' {2,}') __TOKEN_MULTISTRING_REPLACE = ' ' def xs_token(s: str) -> str: """Make a ``token``, adhering XML spec. .. epigraph:: *token* represents tokenized strings. The `·value space· `_ of token is the set of strings that do not contain the carriage return (#xD), line feed (#xA) nor tab (#x9) characters, that have no leading or trailing spaces (#x20) and that have no internal sequences of two or more spaces. The `·lexical space· `_ of token is the set of strings that do not contain the carriage return (#xD), line feed (#xA) nor tab (#x9) characters, that have no leading or trailing spaces (#x20) and that have no internal sequences of two or more spaces. The `·base type· `_ of token is `normalizedString `_. -- the `XML schema spec `_ """ return __TOKEN_MULTISTRING_SEARCH.sub( __TOKEN_MULTISTRING_REPLACE, xs_normalizedString(s).strip()) # endregion serializable-2.0.0/pyproject.toml000066400000000000000000000067531475213023200171050ustar00rootroot00000000000000[build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] name = "py-serializable" version = "2.0.0" description = "Library for serializing and deserializing Python Objects to and from JSON and XML." authors = ["Paul Horton "] maintainers = [ "Jan Kowalleck ", ] license = "Apache-2.0" readme = "README.md" homepage = "https://github.com/madpah/serializable#readme" repository = "https://github.com/madpah/serializable" documentation = "https://py-serializable.readthedocs.io/" packages = [ { include = "py_serializable" } ] include = [ # all is an object -> prevent parse issue with dependabot { path="README.md", format =["sdist"] }, { path="CHANGELOG.md", format=["sdist"] }, { path="docs", format=["sdist"] }, { path="tests", format=["sdist"] }, ] exclude = [ # exclude dotfiles and dotfolders "**/.*", "docs/_build", ] classifiers = [ # Trove classifiers - https://packaging.python.org/specifications/core-metadata/#metadata-classifier # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'Topic :: Software Development', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Typing :: Typed' ] keywords = [ "serialization", "deserialization", "JSON", "XML" ] [tool.poetry.urls] "Bug Tracker" = "https://github.com/madpah/serializable/issues" [tool.poetry.dependencies] python = "^3.8" defusedxml = "^0.7.1" [tool.poetry.dev-dependencies] tox = "4.11.4" coverage = "7.6.1" xmldiff = "2.7.0" mypy = "1.14.1" autopep8 = "2.3.1" isort = "5.13.2" flake8 = { version="7.1.1", python=">=3.8.1" } flake8-annotations = { version="3.1.1", python=">=3.8.1" } flake8-bugbear = { version="24.12.12", python=">=3.8.1" } flake8-isort = "6.1.1" flake8-quotes = "3.4.0" flake8-use-fstring = "1.4" flake8-logging = "1.6.0" [tool.semantic_release] # see https://python-semantic-release.readthedocs.io/en/latest/configuration.html commit_author = "semantic-release " commit_message = "chore(release): {version}\n\nAutomatically generated by python-semantic-release\n\nSigned-off-by: semantic-release " upload_to_vcs_release = true build_command = "pip install poetry && poetry build" version_toml = ["pyproject.toml:tool.poetry.version"] version_variables = [ "py_serializable/__init__.py:__version__", "docs/conf.py:release", ] [tool.semantic_release.publish] dist_glob_patterns = ["dist/*"] upload_to_vcs_release = true [tool.semantic_release.changelog] changelog_file = "CHANGELOG.md" exclude_commit_patterns = [ '''chore(?:\([^)]*?\))?: .+''', '''ci(?:\([^)]*?\))?: .+''', '''refactor(?:\([^)]*?\))?: .+''', '''style(?:\([^)]*?\))?: .+''', '''tests?(?:\([^)]*?\))?: .+''', '''build\((?!deps\): .+)''', ] [tool.semantic_release.branches.main] match = "(main|master)" prerelease = false [tool.semantic_release.branches."step"] match = "(build|chore|ci|docs|feat|fix|perf|style|refactor|test)" prerelease = true prerelease_token = "alpha" [tool.semantic_release.branches."major-dev"] match = "(\\d+\\.0\\.0-(dev|rc)|dev/\\d+\\.0\\.0)" prerelease = true prerelease_token = "rc" serializable-2.0.0/tests/000077500000000000000000000000001475213023200153205ustar00rootroot00000000000000serializable-2.0.0/tests/__init__.py000066400000000000000000000012661475213023200174360ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. serializable-2.0.0/tests/base.py000066400000000000000000000070351475213023200166110ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. import json import os from typing import Any, Optional, Union from unittest import TestCase import lxml # type: ignore from defusedxml import ElementTree as SafeElementTree # type: ignore from xmldiff import main # type: ignore from xmldiff.actions import MoveNode # type: ignore FIXTURES_DIRECTORY = os.path.join(os.path.dirname(__file__), 'fixtures') class BaseTestCase(TestCase): @staticmethod def _sort_json_dict(item: object) -> Any: if isinstance(item, dict): return sorted((key, BaseTestCase._sort_json_dict(values)) for key, values in item.items()) if isinstance(item, list): return sorted(BaseTestCase._sort_json_dict(x) for x in item) else: return item def assertEqualJson(self, expected: str, actual: str) -> None: self.assertEqual( BaseTestCase._sort_json_dict(json.loads(expected)), BaseTestCase._sort_json_dict(json.loads(actual)) ) def assertEqualXml(self, expected: str, actual: str) -> None: a = SafeElementTree.tostring( SafeElementTree.fromstring(expected, lxml.etree.XMLParser(remove_blank_text=True, remove_comments=True)), 'unicode' ) b = SafeElementTree.tostring( SafeElementTree.fromstring(actual, lxml.etree.XMLParser(remove_blank_text=True, remove_comments=True)), 'unicode' ) diff_results = main.diff_texts(a, b, diff_options={'F': 0.5}) diff_results = list(filter(lambda o: not isinstance(o, MoveNode), diff_results)) self.assertEqual(len(diff_results), 0, f'There are XML differences: {diff_results!r}\n- {a!s}\n+ {b!s}') class DeepCompareMixin(object): def assertDeepEqual(self: Union[TestCase, 'DeepCompareMixin'], first: Any, second: Any, msg: Optional[str] = None) -> None: """costly compare, but very verbose""" _omd = self.maxDiff try: self.maxDiff = None dd1 = self.__deepDict(first) dd2 = self.__deepDict(second) self.assertDictEqual(dd1, dd2, msg) finally: self.maxDiff = _omd def __deepDict(self, o: Any) -> Any: if isinstance(o, tuple): return tuple(self.__deepDict(i) for i in o) if isinstance(o, list): return list(self.__deepDict(i) for i in o) if isinstance(o, dict): return {k: self.__deepDict(v) for k, v in o.items()} if isinstance(o, set): # this method returns dict. `dict` is not hashable, so use `tuple` instead. return tuple(self.__deepDict(i) for i in sorted(o, key=hash)) + ('%conv:%set',) if hasattr(o, '__dict__'): d = {a: self.__deepDict(v) for a, v in o.__dict__.items() if '__' not in a} d['%conv'] = str(type(o)) return d return o serializable-2.0.0/tests/fixtures/000077500000000000000000000000001475213023200171715ustar00rootroot00000000000000serializable-2.0.0/tests/fixtures/the-phoenix-project-bookedition-none.json000066400000000000000000000005721475213023200272250ustar00rootroot00000000000000{ "authors": [ "George Spafford", "Gene Kim", "Kevin Behr" ], "edition": { "name": "Preview Edition" }, "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", "isbnNumber": "978-1942788294", "publishDate": "2018-04-16", "publisher": { "name": "IT Revolution Press LLC" }, "rating": 9.8, "title": "{J} The Phoenix Project", "type": "fiction" } serializable-2.0.0/tests/fixtures/the-phoenix-project-bookedition-none.xml000066400000000000000000000006741475213023200270570ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d {X} The Phoenix Project Preview Edition 2018-04-16 Kevin Behr George Spafford Gene Kim fiction IT Revolution Press LLC 9.8 serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-1-v1.xml000066400000000000000000000020011475213023200262520ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr Gene Kim George Spafford fiction IT Revolution Press LLC 1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-1-v2.xml000066400000000000000000000020551475213023200262640ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr Gene Kim George Spafford fiction IT Revolution Press LLC
10 Downing Street
1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8
serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-1-v3.xml000066400000000000000000000020151475213023200262610ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr Gene Kim George Spafford fiction IT Revolution Press LLC RUBBISH 1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-1-v4.xml000066400000000000000000000026261475213023200262720ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr Gene Kim George Spafford fiction IT Revolution Press LLC
10 Downing Street
1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 stock-id-1 stock-id-2
serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-1.xml000066400000000000000000000017561475213023200257460ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr Gene Kim George Spafford fiction IT Revolution Press LLC 1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-references.json000066400000000000000000000017621475213023200300750ustar00rootroot00000000000000{ "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", "title": "{J} The Phoenix Project", "isbnNumber": "978-1942788294", "rating": 9.8, "edition": { "number": 5, "name": "5th Anniversary Limited Edition" }, "publishDate": "2018-04-16", "type": "fiction", "authors": [ "Kevin Behr", "Gene Kim", "George Spafford" ], "publisher": { "name": "IT Revolution Press LLC", "address": "10 Downing Street" }, "chapters": [ { "number": 1, "title": "Tuesday, September 2" }, { "number": 2, "title": "Tuesday, September 2" }, { "number": 3, "title": "Tuesday, September 2" }, { "number": 4, "title": "Wednesday, September 3" } ], "references": [ { "reference": "my-ref-3", "refersTo": [ "sub-ref-2" ] }, { "reference": "my-ref-2", "refersTo": [ "sub-ref-1", "sub-ref-3" ] }, { "reference": "my-ref-1" } ] }serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-v2.json000066400000000000000000000013771475213023200263050ustar00rootroot00000000000000{ "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", "title": "{J} The Phoenix Project", "isbnNumber": "978-1942788294", "rating": 9.8, "edition": { "number": 5, "name": "5th Anniversary Limited Edition" }, "publishDate": "2018-04-16", "type": "fiction", "authors": [ "Kevin Behr", "Gene Kim", "George Spafford" ], "publisher": { "name": "IT Revolution Press LLC", "email": null, "address": "10 Downing Street" }, "chapters": [ { "number": 1, "title": "Tuesday, September 2" }, { "number": 2, "title": "Tuesday, September 2" }, { "number": 3, "title": "Tuesday, September 2" }, { "number": 4, "title": "Wednesday, September 3" } ] }serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-v3.json000066400000000000000000000013401475213023200262740ustar00rootroot00000000000000{ "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", "title": "{J} The Phoenix Project", "isbnNumber": "978-1942788294", "rating": 9.8, "edition": { "number": 5, "name": "5th Anniversary Limited Edition" }, "publishDate": "2018-04-16", "type": "fiction", "authors": [ "Kevin Behr", "Gene Kim", "George Spafford" ], "publisher": { "name": "IT Revolution Press LLC", "email": "RUBBISH" }, "chapters": [ { "number": 1, "title": "Tuesday, September 2" }, { "number": 2, "title": "Tuesday, September 2" }, { "number": 3, "title": "Tuesday, September 2" }, { "number": 4, "title": "Wednesday, September 3" } ] }serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-v4.json000066400000000000000000000020531475213023200262770ustar00rootroot00000000000000{ "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", "title": "{J} The Phoenix Project", "isbnNumber": "978-1942788294", "rating": 9.8, "edition": { "number": 5, "name": "5th Anniversary Limited Edition" }, "stockIds": [ "stock-id-1", "stock-id-2" ], "publishDate": "2018-04-16", "type": "fiction", "authors": [ "Kevin Behr", "Gene Kim", "George Spafford" ], "publisher": { "name": "IT Revolution Press LLC", "address": "10 Downing Street" }, "chapters": [ { "number": 1, "title": "Tuesday, September 2" }, { "number": 2, "title": "Tuesday, September 2" }, { "number": 3, "title": "Tuesday, September 2" }, { "number": 4, "title": "Wednesday, September 3" } ], "references": [ { "reference": "my-ref-3", "refersTo": [ "sub-ref-2" ] }, { "reference": "my-ref-2", "refersTo": [ "sub-ref-1", "sub-ref-3" ] }, { "reference": "my-ref-1" } ] } serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-with-ignored.json000066400000000000000000000013031475213023200303430ustar00rootroot00000000000000{ "something_to_be_ignored": "some_value", "title": "{J} The Phoenix Project", "rating": 9.8, "isbnNumber": "978-1942788294", "edition": { "number": 5, "name": "5th Anniversary Limited Edition" }, "publishDate": "2018-04-16", "type": "fiction", "authors": [ "Kevin Behr", "Gene Kim", "George Spafford" ], "publisher": { "name": "IT Revolution Press LLC" }, "chapters": [ { "number": 1, "title": "Tuesday, September 2" }, { "number": 2, "title": "Tuesday, September 2" }, { "number": 3, "title": "Tuesday, September 2" }, { "number": 4, "title": "Wednesday, September 3" } ] }serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case-with-ignored.xml000066400000000000000000000017561475213023200302060ustar00rootroot00000000000000 thing {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr Gene Kim George Spafford fiction IT Revolution Press LLC 1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 serializable-2.0.0/tests/fixtures/the-phoenix-project-camel-case.json000066400000000000000000000013101475213023200257430ustar00rootroot00000000000000{ "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", "title": "{J} The Phoenix Project", "rating": 9.8, "isbnNumber": "978-1942788294", "edition": { "number": 5, "name": "5th Anniversary Limited Edition" }, "publishDate": "2018-04-16", "type": "fiction", "authors": [ "Kevin Behr", "Gene Kim", "George Spafford" ], "publisher": { "name": "IT Revolution Press LLC" }, "chapters": [ { "number": 1, "title": "Tuesday, September 2" }, { "number": 2, "title": "Tuesday, September 2" }, { "number": 3, "title": "Tuesday, September 2" }, { "number": 4, "title": "Wednesday, September 3" } ] }serializable-2.0.0/tests/fixtures/the-phoenix-project-defaultNS-isset-v4.xml000066400000000000000000000027061475213023200271530ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Gene Kim George Spafford Kevin Behr fiction
10 Downing Street
IT Revolution Press LLC
1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 stock-id-1 stock-id-2
serializable-2.0.0/tests/fixtures/the-phoenix-project-defaultNS-isset.SNAPSHOT.xml000066400000000000000000000013161475213023200300560ustar00rootroot00000000000000f3758bf0-0ff7-4366-a5e5-c209d4352b2d{X} The Phoenix Project5th Anniversary Limited Edition2018-04-16Karl RanseierfictionIT Revolution Press LLC1Tuesday, September 22Tuesday, September 23Tuesday, September 24Wednesday, September 39.8serializable-2.0.0/tests/fixtures/the-phoenix-project-defaultNS-mixed-v4.xml000066400000000000000000000033141475213023200271260ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Gene Kim George Spafford Kevin Behr fiction 10 Downing Street IT Revolution Press LLC 1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 stock-id-1 stock-id-2 serializable-2.0.0/tests/fixtures/the-phoenix-project-defaultNS-unset-v4.xml000066400000000000000000000032471475213023200271630ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Gene Kim George Spafford Kevin Behr fiction 10 Downing Street IT Revolution Press LLC 1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 stock-id-1 stock-id-2 serializable-2.0.0/tests/fixtures/the-phoenix-project-defaultNS-unset.SNAPSHOT.xml000066400000000000000000000016221475213023200300650ustar00rootroot00000000000000f3758bf0-0ff7-4366-a5e5-c209d4352b2d{X} The Phoenix Project5th Anniversary Limited Edition2018-04-16Karl RanseierfictionIT Revolution Press LLC1Tuesday, September 22Tuesday, September 23Tuesday, September 24Wednesday, September 39.8serializable-2.0.0/tests/fixtures/the-phoenix-project-kebab-case-1-v2.xml000066400000000000000000000020601475213023200262430ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr Gene Kim George Spafford fiction IT Revolution Press LLC
10 Downing Street
1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8
serializable-2.0.0/tests/fixtures/the-phoenix-project-kebab-case-1.xml000066400000000000000000000017611475213023200257250ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr Gene Kim George Spafford fiction IT Revolution Press LLC 1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 serializable-2.0.0/tests/fixtures/the-phoenix-project-kebab-case.json000066400000000000000000000013121475213023200257300ustar00rootroot00000000000000{ "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", "title": "{J} The Phoenix Project", "isbn-number": "978-1942788294", "rating": 9.8, "edition": { "number": 5, "name": "5th Anniversary Limited Edition" }, "publish-date": "2018-04-16", "type": "fiction", "authors": [ "Kevin Behr", "Gene Kim", "George Spafford" ], "publisher": { "name": "IT Revolution Press LLC" }, "chapters": [ { "number": 1, "title": "Tuesday, September 2" }, { "number": 2, "title": "Tuesday, September 2" }, { "number": 3, "title": "Tuesday, September 2" }, { "number": 4, "title": "Wednesday, September 3" } ] }serializable-2.0.0/tests/fixtures/the-phoenix-project-snake-case-1.xml000066400000000000000000000017611475213023200257620ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr Gene Kim George Spafford fiction IT Revolution Press LLC 1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 serializable-2.0.0/tests/fixtures/the-phoenix-project-snake-case.json000066400000000000000000000013121475213023200257650ustar00rootroot00000000000000{ "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", "title": "{J} The Phoenix Project", "rating": 9.8, "isbn_number": "978-1942788294", "edition": { "number": 5, "name": "5th Anniversary Limited Edition" }, "publish_date": "2018-04-16", "type": "fiction", "authors": [ "Kevin Behr", "Gene Kim", "George Spafford" ], "publisher": { "name": "IT Revolution Press LLC" }, "chapters": [ { "number": 1, "title": "Tuesday, September 2" }, { "number": 2, "title": "Tuesday, September 2" }, { "number": 3, "title": "Tuesday, September 2" }, { "number": 4, "title": "Wednesday, September 3" } ] }serializable-2.0.0/tests/fixtures/the-phoenix-project_unnormalized-input_v4.xml000066400000000000000000000032441475213023200301470ustar00rootroot00000000000000 f3758bf0-0ff7-4366-a5e5-c209d4352b2d {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr Gene Kim George Spafford fiction IT Revolution Press LLC
10 Downing Street
1 Tuesday, September 2 2 Tuesday, September 2 3 Tuesday, September 2 4 Wednesday, September 3 9.8 stock-id-1 stock-id-2
serializable-2.0.0/tests/model.py000066400000000000000000000372011475213023200167750ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. import re from datetime import date from decimal import Decimal from enum import Enum, unique from typing import Any, Dict, Iterable, List, Optional, Set, Type from uuid import UUID, uuid4 import py_serializable from py_serializable import ViewType, XmlArraySerializationType, XmlStringSerializationType from py_serializable.helpers import BaseHelper, Iso8601Date """ Model classes used in unit tests and examples. """ class SchemaVersion1(ViewType): pass class SchemaVersion2(ViewType): pass class SchemaVersion3(ViewType): pass class SchemaVersion4(ViewType): pass SCHEMAVERSION_MAP: Dict[int, Type[ViewType]] = { 1: SchemaVersion1, 2: SchemaVersion2, 3: SchemaVersion3, 4: SchemaVersion4, } class ReferenceReferences(BaseHelper): @classmethod def serialize(cls, o: Any) -> Set[str]: if isinstance(o, set): return set(map(lambda i: str(i.ref), o)) raise ValueError(f'Attempt to serialize a non-set: {o.__class__}') @classmethod def deserialize(cls, o: Any) -> Set['BookReference']: print(f'Deserializing {o} ({type(o)})') references: Set['BookReference'] = set() if isinstance(o, list): for v in o: references.add(BookReference(ref=v)) return references raise ValueError(f'Attempt to deserialize a non-set: {o.__class__}') class TitleMapper(BaseHelper): @classmethod def json_serialize(cls, o: str) -> str: return f'{{J}} {o}' @classmethod def json_deserialize(cls, o: str) -> str: return re.sub(r'^\{J} ', '', o) @classmethod def xml_serialize(cls, o: str) -> str: return f'{{X}} {o}' @classmethod def xml_deserialize(cls, o: str) -> str: return re.sub(r'^\{X} ', '', o) class BookEditionHelper(BaseHelper): @classmethod def serialize(cls, o: Any) -> Optional[int]: return o \ if isinstance(o, int) and o > 0 \ else None @classmethod def deserialize(cls, o: Any) -> int: try: return int(o) except Exception: return 1 @py_serializable.serializable_class class Chapter: def __init__(self, *, number: int, title: str) -> None: self._number = number self._title = title @property def number(self) -> int: return self._number @property @py_serializable.xml_string(XmlStringSerializationType.TOKEN) def title(self) -> str: return self._title def __eq__(self, other: Any) -> bool: if isinstance(other, Chapter): return hash(other) == hash(self) return False def __hash__(self) -> int: return hash((self.number, self.title)) @py_serializable.serializable_class class Publisher: def __init__(self, *, name: str, address: Optional[str] = None, email: Optional[str] = None) -> None: self._name = name self._address = address self._email = email @property def name(self) -> str: return self._name @property @py_serializable.view(SchemaVersion2) @py_serializable.view(SchemaVersion4) def address(self) -> Optional[str]: return self._address @property @py_serializable.include_none(SchemaVersion2) @py_serializable.include_none(SchemaVersion3, 'RUBBISH') def email(self) -> Optional[str]: return self._email def __eq__(self, other: object) -> bool: if isinstance(other, Publisher): return hash(other) == hash(self) return False def __hash__(self) -> int: return hash((self.name, self.address, self.email)) @unique class BookType(Enum): FICTION = 'fiction' NON_FICTION = 'non-fiction' @py_serializable.serializable_class(name='edition') class BookEdition: def __init__(self, *, number: int, name: str) -> None: self._number = number self._name = name @property @py_serializable.xml_attribute() @py_serializable.type_mapping(BookEditionHelper) def number(self) -> int: return self._number @property @py_serializable.xml_name('.') def name(self) -> str: return self._name def __eq__(self, other: object) -> bool: if isinstance(other, BookEdition): return hash(other) == hash(self) return False def __hash__(self) -> int: return hash((self.number, self.name)) @py_serializable.serializable_class class BookReference: def __init__(self, *, ref: str, references: Optional[Iterable['BookReference']] = None) -> None: self.ref = ref self.references = set(references or {}) @property @py_serializable.json_name('reference') @py_serializable.xml_attribute() @py_serializable.xml_string(XmlStringSerializationType.TOKEN) def ref(self) -> str: return self._ref @ref.setter def ref(self, ref: str) -> None: self._ref = ref @property @py_serializable.json_name('refersTo') @py_serializable.type_mapping(ReferenceReferences) @py_serializable.xml_array(py_serializable.XmlArraySerializationType.FLAT, 'reference') def references(self) -> Set['BookReference']: return self._references @references.setter def references(self, references: Iterable['BookReference']) -> None: self._references = set(references) def __eq__(self, other: object) -> bool: if isinstance(other, BookReference): return hash(other) == hash(self) return False def __hash__(self) -> int: return hash((self.ref, tuple(self.references))) def __repr__(self) -> str: return f'' @py_serializable.serializable_class class StockId(py_serializable.helpers.BaseHelper): def __init__(self, id: str) -> None: self._id = id @property @py_serializable.json_name('.') @py_serializable.xml_name('.') def id(self) -> str: return self._id @classmethod def serialize(cls, o: Any) -> str: if isinstance(o, StockId): return str(o) raise Exception( f'Attempt to serialize a non-StockId: {o!r}') @classmethod def deserialize(cls, o: Any) -> 'StockId': try: return StockId(id=str(o)) except ValueError as err: raise Exception( f'StockId string supplied does not parse: {o!r}' ) from err def __eq__(self, other: Any) -> bool: if isinstance(other, StockId): return hash(other) == hash(self) return False def __lt__(self, other: Any) -> bool: if isinstance(other, StockId): return self._id < other._id return NotImplemented def __hash__(self) -> int: return hash(self._id) def __repr__(self) -> str: return f'' def __str__(self) -> str: return self._id @py_serializable.serializable_class(name='bigbook', ignore_during_deserialization=['something_to_be_ignored', 'ignore_me', 'ignored']) class Book: def __init__(self, title: str, isbn: str, publish_date: date, authors: Iterable[str], publisher: Optional[Publisher] = None, chapters: Optional[Iterable[Chapter]] = None, edition: Optional[BookEdition] = None, type: BookType = BookType.FICTION, id: Optional[UUID] = None, references: Optional[Iterable[BookReference]] = None, rating: Optional[Decimal] = None, stock_ids: Optional[Iterable[StockId]] = None) -> None: self._id = id or uuid4() self._title = title self._isbn = isbn self._edition = edition self._publish_date = publish_date self._authors = set(authors) self._publisher = publisher self.chapters = list(chapters or []) self._type = type self.references = set(references or []) self.rating = Decimal('NaN') if rating is None else rating self._stock_ids = set(stock_ids or []) @property @py_serializable.xml_sequence(1) def id(self) -> UUID: return self._id @property @py_serializable.xml_sequence(2) @py_serializable.type_mapping(TitleMapper) @py_serializable.xml_string(XmlStringSerializationType.TOKEN) def title(self) -> str: return self._title @property @py_serializable.json_name('isbn_number') @py_serializable.xml_attribute() @py_serializable.xml_name('isbn_number') def isbn(self) -> str: return self._isbn @property @py_serializable.xml_sequence(3) def edition(self) -> Optional[BookEdition]: return self._edition @property @py_serializable.xml_sequence(4) @py_serializable.type_mapping(Iso8601Date) def publish_date(self) -> date: return self._publish_date @property @py_serializable.xml_array(XmlArraySerializationType.FLAT, 'author') @py_serializable.xml_string(XmlStringSerializationType.NORMALIZED_STRING) @py_serializable.xml_sequence(5) def authors(self) -> Set[str]: return self._authors @property @py_serializable.xml_sequence(7) def publisher(self) -> Optional[Publisher]: return self._publisher @property @py_serializable.xml_array(XmlArraySerializationType.NESTED, 'chapter') @py_serializable.xml_sequence(8) def chapters(self) -> List[Chapter]: return self._chapters @chapters.setter def chapters(self, chapters: Iterable[Chapter]) -> None: self._chapters = list(chapters) @property @py_serializable.xml_sequence(6) def type(self) -> BookType: return self._type @property @py_serializable.view(SchemaVersion4) @py_serializable.xml_array(py_serializable.XmlArraySerializationType.NESTED, 'reference') @py_serializable.xml_sequence(7) def references(self) -> Set[BookReference]: return self._references @references.setter def references(self, references: Iterable[BookReference]) -> None: self._references = set(references) @property @py_serializable.xml_sequence(20) def rating(self) -> Decimal: return self._rating @rating.setter def rating(self, rating: Decimal) -> None: self._rating = rating @property @py_serializable.view(SchemaVersion4) @py_serializable.xml_array(XmlArraySerializationType.FLAT, 'stockId') @py_serializable.xml_sequence(21) def stock_ids(self) -> Set[StockId]: return self._stock_ids # region ThePhoenixProject_v2 ThePhoenixProject_v1 = Book( title='The Phoenix Project', isbn='978-1942788294', publish_date=date(year=2018, month=4, day=16), authors=['Gene Kim', 'Kevin Behr', 'George Spafford'], publisher=Publisher(name='IT Revolution Press LLC'), edition=BookEdition(number=5, name='5th Anniversary Limited Edition'), id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'), rating=Decimal('9.8') ) ThePhoenixProject_v1.chapters.append(Chapter(number=1, title='Tuesday, September 2')) ThePhoenixProject_v1.chapters.append(Chapter(number=2, title='Tuesday, September 2')) ThePhoenixProject_v1.chapters.append(Chapter(number=3, title='Tuesday, September 2')) ThePhoenixProject_v1.chapters.append(Chapter(number=4, title='Wednesday, September 3')) # endregion ThePhoenixProject_v2 # region ThePhoenixProject_v2 ThePhoenixProject_v2 = Book( title='The Phoenix Project', isbn='978-1942788294', publish_date=date(year=2018, month=4, day=16), authors=['Gene Kim', 'Kevin Behr', 'George Spafford'], publisher=Publisher(name='IT Revolution Press LLC', address='10 Downing Street'), edition=BookEdition(number=5, name='5th Anniversary Limited Edition'), id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'), rating=Decimal('9.8'), stock_ids=[StockId('stock-id-1'), StockId('stock-id-2')] ) ThePhoenixProject_v2.chapters.append(Chapter(number=1, title='Tuesday, September 2')) ThePhoenixProject_v2.chapters.append(Chapter(number=2, title='Tuesday, September 2')) ThePhoenixProject_v2.chapters.append(Chapter(number=3, title='Tuesday, September 2')) ThePhoenixProject_v2.chapters.append(Chapter(number=4, title='Wednesday, September 3')) SubRef1 = BookReference(ref='sub-ref-1') SubRef2 = BookReference(ref='sub-ref-2') SubRef3 = BookReference(ref='sub-ref-3') Ref1 = BookReference(ref='my-ref-1') Ref2 = BookReference(ref='my-ref-2', references=[SubRef1, SubRef3]) Ref3 = BookReference(ref='my-ref-3', references=[SubRef2]) ThePhoenixProject_v2.references = {Ref3, Ref2, Ref1} # endregion ThePhoenixProject_v2 ThePhoenixProject = ThePhoenixProject_v2 # region ThePhoenixProject_unnormalized # a case where the `normalizedString` and `token` transformation must come into play ThePhoenixProject_unnormalized = Book( title='The \n Phoenix Project ', isbn='978-1942788294', publish_date=date(year=2018, month=4, day=16), authors=['Gene Kim', 'Kevin\r\nBehr', 'George\tSpafford'], publisher=Publisher(name='IT Revolution Press LLC', address='10 Downing Street'), edition=BookEdition(number=5, name='5th Anniversary Limited Edition'), id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'), rating=Decimal('9.8'), stock_ids=[StockId('stock-id-1'), StockId('stock-id-2')] ) ThePhoenixProject_unnormalized.chapters.append(Chapter(number=1, title='Tuesday, September 2')) ThePhoenixProject_unnormalized.chapters.append(Chapter(number=2, title='Tuesday,\tSeptember 2')) ThePhoenixProject_unnormalized.chapters.append(Chapter(number=3, title='Tuesday,\r\nSeptember 2')) ThePhoenixProject_unnormalized.chapters.append(Chapter(number=4, title='Wednesday,\rSeptember\n3')) SubRef1 = BookReference(ref=' sub-ref-1 ') SubRef2 = BookReference(ref='\rsub-ref-2\t') SubRef3 = BookReference(ref='\nsub-ref-3\r\n') Ref1 = BookReference(ref='\r\nmy-ref-1') Ref2 = BookReference(ref='\tmy-ref-2', references=[SubRef1, SubRef3]) Ref3 = BookReference(ref=' my-ref-3\n', references=[SubRef2]) ThePhoenixProject_unnormalized.references = {Ref3, Ref2, Ref1} # endregion ThePhoenixProject_unnormalized # region ThePhoenixProject_attr_serialized_none # a case where an attribute is serialized to `None` and deserialized from it ThePhoenixProject_attr_serialized_none = Book( title='The Phoenix Project', isbn='978-1942788294', publish_date=date(year=2018, month=4, day=16), authors=['Gene Kim', 'Kevin Behr', 'George Spafford'], publisher=Publisher(name='IT Revolution Press LLC'), edition=BookEdition(number=0, name='Preview Edition'), id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'), rating=Decimal('9.8') ) # endregion ThePhoenixProject_attr_serialized_none if __name__ == '__main__': tpp_as_xml = ThePhoenixProject.as_xml() # type:ignore[attr-defined] tpp_as_json = ThePhoenixProject.as_json() # type:ignore[attr-defined] print(tpp_as_xml, tpp_as_json, sep='\n\n') import io import json tpp_from_xml = ThePhoenixProject.from_xml( # type:ignore[attr-defined] io.StringIO(tpp_as_xml)) tpp_from_json = ThePhoenixProject.from_json( # type:ignore[attr-defined] json.loads(tpp_as_json)) serializable-2.0.0/tests/test_formatters.py000066400000000000000000000070711475213023200211240ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. from unittest import TestCase from py_serializable.formatters import ( CamelCasePropertyNameFormatter, KebabCasePropertyNameFormatter, SnakeCasePropertyNameFormatter, ) class TestFormatterCamelCase(TestCase): def test_encode_1(self) -> None: self.assertEqual('bookChapters', CamelCasePropertyNameFormatter.encode(property_name='book_chapters')) def test_encode_2(self) -> None: self.assertEqual('id', CamelCasePropertyNameFormatter.encode(property_name='id')) def test_encode_3(self) -> None: self.assertEqual('book', CamelCasePropertyNameFormatter.encode(property_name='Book')) def test_encode_4(self) -> None: self.assertEqual('type', CamelCasePropertyNameFormatter.encode(property_name='type')) def test_decode_1(self) -> None: self.assertEqual('book_chapters', CamelCasePropertyNameFormatter.decode(property_name='bookChapters')) def test_decode_2(self) -> None: self.assertEqual('id', CamelCasePropertyNameFormatter.decode(property_name='id')) def test_decode_4(self) -> None: self.assertEqual('type', CamelCasePropertyNameFormatter.decode(property_name='type')) def test_decode_class_name_1(self) -> None: self.assertEqual('Book', CamelCasePropertyNameFormatter.decode_as_class_name(name='book')) def test_decode_class_name_2(self) -> None: self.assertEqual('BookChapters', CamelCasePropertyNameFormatter.decode_as_class_name(name='bookChapters')) class TestFormatterKebabCase(TestCase): def test_encode_1(self) -> None: self.assertEqual('book-chapters', KebabCasePropertyNameFormatter.encode(property_name='book_chapters')) def test_encode_2(self) -> None: self.assertEqual('id', KebabCasePropertyNameFormatter.encode(property_name='id')) def test_encode_3(self) -> None: self.assertEqual('book', KebabCasePropertyNameFormatter.encode(property_name='Book')) def test_decode_1(self) -> None: self.assertEqual('book_chapters', KebabCasePropertyNameFormatter.decode(property_name='book-chapters')) def test_decode_2(self) -> None: self.assertEqual('id', KebabCasePropertyNameFormatter.decode(property_name='id')) class TestFormatterSnakeCase(TestCase): def test_encode_1(self) -> None: self.assertEqual('book_chapters', SnakeCasePropertyNameFormatter.encode(property_name='book_chapters')) def test_encode_2(self) -> None: self.assertEqual('id', SnakeCasePropertyNameFormatter.encode(property_name='id')) def test_encode_3(self) -> None: self.assertEqual('book', SnakeCasePropertyNameFormatter.encode(property_name='Book')) def test_decode_1(self) -> None: self.assertEqual('book_chapters', SnakeCasePropertyNameFormatter.decode(property_name='book_chapters')) def test_decode_2(self) -> None: self.assertEqual('id', SnakeCasePropertyNameFormatter.decode(property_name='id')) serializable-2.0.0/tests/test_helpers.py000066400000000000000000000166161475213023200204050ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. from datetime import date, datetime, timedelta, timezone from unittest import TestCase from py_serializable import logger from py_serializable.helpers import Iso8601Date, XsdDate, XsdDateTime class TestIso8601Date(TestCase): def test_serialize_date(self) -> None: self.assertEqual( Iso8601Date.serialize(date(year=2022, month=8, day=3)), '2022-08-03' ) def test_serialize_datetime(self) -> None: self.assertEqual( Iso8601Date.serialize(datetime(year=2022, month=8, day=3)), '2022-08-03' ) def test_deserialize_valid_date(self) -> None: self.assertEqual( Iso8601Date.deserialize('2022-08-03'), date(year=2022, month=8, day=3) ) def test_deserialize_valid(self) -> None: with self.assertRaises(ValueError): Iso8601Date.deserialize('2022-08-03zzz') class TestXsdDate(TestCase): """ See: http://books.xmlschemata.org/relaxng/ch19-77041.html """ def test_deserialize_valid_1(self) -> None: self.assertEqual( XsdDate.deserialize('2001-10-26'), date(year=2001, month=10, day=26) ) def test_deserialize_valid_2(self) -> None: with self.assertLogs(logger) as logs: self.assertEqual( XsdDate.deserialize('2001-10-26+02:00'), date(year=2001, month=10, day=26) ) self.assertIn( 'WARNING:py_serializable.helpers:' 'Potential data loss will occur: dates with timezones not supported in Python', logs.output) def test_deserialize_valid_3(self) -> None: with self.assertLogs(logger) as logs: self.assertEqual( XsdDate.deserialize('2001-10-26Z'), date(year=2001, month=10, day=26) ) self.assertIn( 'WARNING:py_serializable.helpers:' 'Potential data loss will occur: dates with timezones not supported in Python', logs.output) def test_deserialize_valid_4(self) -> None: with self.assertLogs(logger) as logs: self.assertEqual( XsdDate.deserialize('2001-10-26+00:00'), date(year=2001, month=10, day=26) ) self.assertIn( 'WARNING:py_serializable.helpers:' 'Potential data loss will occur: dates with timezones not supported in Python', logs.output) def test_deserialize_valid_5(self) -> None: self.assertEqual( XsdDate.deserialize('-2001-10-26'), date(year=2001, month=10, day=26) ) def test_serialize_1(self) -> None: self.assertEqual( XsdDate.serialize(date(year=2001, month=10, day=26)), '2001-10-26' ) class TestXsdDateTime(TestCase): """ See: http://books.xmlschemata.org/relaxng/ch19-77049.html """ def test_deserialize_valid_1(self) -> None: self.assertEqual( XsdDateTime.deserialize('2001-10-26T21:32:52'), datetime(year=2001, month=10, day=26, hour=21, minute=32, second=52, tzinfo=None) ) def test_deserialize_valid_2(self) -> None: self.assertEqual( XsdDateTime.deserialize('2001-10-26T21:32:52+02:00'), datetime( year=2001, month=10, day=26, hour=21, minute=32, second=52, tzinfo=timezone(timedelta(seconds=7200)) ) ) def test_deserialize_valid_3(self) -> None: self.assertEqual( XsdDateTime.deserialize('2001-10-26T19:32:52Z'), datetime(year=2001, month=10, day=26, hour=19, minute=32, second=52, tzinfo=timezone.utc) ) def test_deserialize_valid_4(self) -> None: self.assertEqual( XsdDateTime.deserialize('2001-10-26T19:32:52+00:00'), datetime(year=2001, month=10, day=26, hour=19, minute=32, second=52, tzinfo=timezone.utc) ) def test_deserialize_valid_5(self) -> None: self.assertEqual( XsdDateTime.deserialize('-2001-10-26T21:32:52'), datetime(year=2001, month=10, day=26, hour=21, minute=32, second=52, tzinfo=None) ) def test_deserialize_valid_6(self) -> None: """Test that less than 6 decimal places in the seconds field is parsed correctly.""" self.assertEqual( XsdDateTime.deserialize('2001-10-26T21:32:52.12679'), datetime(year=2001, month=10, day=26, hour=21, minute=32, second=52, microsecond=126790, tzinfo=None) ) def test_deserialize_valid_7(self) -> None: """Test that exactly 6 decimal places in the seconds field is parsed correctly.""" self.assertEqual( XsdDateTime.deserialize('2024-09-23T08:06:09.185596Z'), datetime(year=2024, month=9, day=23, hour=8, minute=6, second=9, microsecond=185596, tzinfo=timezone.utc) ) def test_deserialize_valid_8(self) -> None: """Test that more than 6 decimal places in the seconds field is parsed correctly.""" self.assertEqual( # values are chosen to showcase rounding on microseconds XsdDateTime.deserialize('2024-09-23T08:06:09.185596536Z'), datetime(year=2024, month=9, day=23, hour=8, minute=6, second=9, microsecond=185597, tzinfo=timezone.utc) ) def test_deserialize_valid_9(self) -> None: """Test that a lot more than 6 decimal places in the seconds is parsed correctly.""" self.assertEqual( # values are chosen to showcase rounding on microseconds XsdDateTime.deserialize('2024-09-23T08:06:09.18559653666666666666666666666666'), datetime(year=2024, month=9, day=23, hour=8, minute=6, second=9, microsecond=185597, tzinfo=None) ) def test_serialize_1(self) -> None: serialized = XsdDateTime.serialize( # assume winter time datetime(year=2001, month=2, day=26, hour=21, minute=32, second=52, microsecond=12679, tzinfo=None) ) self.assertRegex(serialized, r'2001-02-26T21:32:52.012679(?:Z|[+-]\d\d:\d\d)') def test_serialize_2(self) -> None: serialized = XsdDateTime.serialize( # assume summer time datetime(year=2001, month=7, day=26, hour=21, minute=32, second=52, microsecond=12679, tzinfo=None) ) self.assertRegex(serialized, r'2001-07-26T21:32:52.012679(?:Z|[+-]\d\d:\d\d)') def test_serialize_3(self) -> None: serialized = XsdDateTime.serialize( datetime( year=2001, month=10, day=26, hour=21, minute=32, second=52, microsecond=12679, tzinfo=timezone.utc ) ) self.assertEqual(serialized, '2001-10-26T21:32:52.012679+00:00') serializable-2.0.0/tests/test_json.py000066400000000000000000000201761475213023200177100ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. import json import os from py_serializable.formatters import ( CamelCasePropertyNameFormatter, CurrentFormatter, KebabCasePropertyNameFormatter, SnakeCasePropertyNameFormatter, ) from tests.base import FIXTURES_DIRECTORY, BaseTestCase from tests.model import ( Book, SchemaVersion2, SchemaVersion3, SchemaVersion4, ThePhoenixProject, ThePhoenixProject_attr_serialized_none, ThePhoenixProject_v1, ) class TestJson(BaseTestCase): # region test_serialize def test_serialize_tfp_cc(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case.json')) as expected_json: self.assertEqualJson(expected_json.read(), ThePhoenixProject.as_json()) def test_serialize_tfp_cc_v2(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-v2.json')) as expected_json: self.assertEqualJson(expected_json.read(), ThePhoenixProject.as_json(view_=SchemaVersion2)) def test_serialize_tfp_cc_v3(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-v3.json')) as expected_json: self.assertEqualJson(expected_json.read(), ThePhoenixProject.as_json(view_=SchemaVersion3)) def test_serialize_tfp_cc_v4(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-v4.json')) as expected_json: self.assertEqualJson(expected_json.read(), ThePhoenixProject.as_json(view_=SchemaVersion4)) def test_deserialize_tfp_cc(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case.json')) as input_json: book: Book = Book.from_json(data=json.loads(input_json.read())) self.assertEqual(str(ThePhoenixProject_v1.id), 'f3758bf0-0ff7-4366-a5e5-c209d4352b2d') self.assertEqual(ThePhoenixProject_v1.title, book.title) self.assertEqual(ThePhoenixProject_v1.isbn, book.isbn) self.assertEqual(ThePhoenixProject_v1.edition, book.edition) self.assertEqual(ThePhoenixProject_v1.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject_v1.authors, book.authors) self.assertEqual(ThePhoenixProject_v1.publisher, book.publisher) self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters) self.assertEqual(ThePhoenixProject_v1.references, book.references) self.assertEqual(ThePhoenixProject_v1.rating, book.rating) def test_deserialize_tfp_cc_with_references(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-references.json')) as input_json: book: Book = Book.from_json(data=json.loads(input_json.read())) self.assertEqual(str(ThePhoenixProject.id), 'f3758bf0-0ff7-4366-a5e5-c209d4352b2d') self.assertEqual(ThePhoenixProject.title, book.title) self.assertEqual(ThePhoenixProject.isbn, book.isbn) self.assertEqual(ThePhoenixProject.edition, book.edition) self.assertEqual(ThePhoenixProject.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject.authors, book.authors) self.assertEqual(ThePhoenixProject.publisher, book.publisher) self.assertEqual(ThePhoenixProject.chapters, book.chapters) self.assertEqual(3, len(book.references)) self.assertEqual(ThePhoenixProject.references, book.references) self.assertEqual(ThePhoenixProject.rating, book.rating) def test_deserialize_tfp_cc_with_ignored(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-with-ignored.json')) as input_json: book: Book = Book.from_json(data=json.loads(input_json.read())) self.assertEqual(ThePhoenixProject_v1.title, book.title) self.assertEqual(ThePhoenixProject_v1.isbn, book.isbn) self.assertEqual(ThePhoenixProject_v1.edition, book.edition) self.assertEqual(ThePhoenixProject_v1.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject_v1.authors, book.authors) self.assertEqual(ThePhoenixProject_v1.publisher, book.publisher) self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters) self.assertEqual(ThePhoenixProject_v1.rating, book.rating) def test_serialize_tfp_kc(self) -> None: CurrentFormatter.formatter = KebabCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-kebab-case.json')) as expected_json: self.assertEqualJson(expected_json.read(), ThePhoenixProject.as_json()) def test_deserialize_tfp_kc(self) -> None: CurrentFormatter.formatter = KebabCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-kebab-case.json')) as input_json: book: Book = Book.from_json(data=json.loads(input_json.read())) self.assertEqual(ThePhoenixProject_v1.title, book.title) self.assertEqual(ThePhoenixProject_v1.isbn, book.isbn) self.assertEqual(ThePhoenixProject_v1.edition, book.edition) self.assertEqual(ThePhoenixProject_v1.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject_v1.authors, book.authors) self.assertEqual(ThePhoenixProject_v1.publisher, book.publisher) self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters) self.assertEqual(ThePhoenixProject_v1.rating, book.rating) def test_serialize_tfp_sc(self) -> None: CurrentFormatter.formatter = SnakeCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-snake-case.json')) as expected_json: self.assertEqualJson(expected_json.read(), ThePhoenixProject.as_json()) def test_deserialize_tfp_sc(self) -> None: CurrentFormatter.formatter = SnakeCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-snake-case.json')) as input_json: book: Book = Book.from_json(data=json.loads(input_json.read())) self.assertEqual(ThePhoenixProject_v1.title, book.title) self.assertEqual(ThePhoenixProject_v1.isbn, book.isbn) self.assertEqual(ThePhoenixProject_v1.edition, book.edition) self.assertEqual(ThePhoenixProject_v1.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject_v1.authors, book.authors) self.assertEqual(ThePhoenixProject_v1.publisher, book.publisher) self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters) self.assertEqual(ThePhoenixProject_v1.rating, book.rating) def test_serialize_attr_none(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-bookedition-none.json')) as expected_json: self.assertEqualJson(expected_json.read(), ThePhoenixProject_attr_serialized_none.as_json()) serializable-2.0.0/tests/test_oml.py000066400000000000000000000027751475213023200175330ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. from typing import Optional from unittest import TestCase from py_serializable import ObjectMetadataLibrary class TestOmlSp(TestCase): def test_prop_primitive_int_1(self) -> None: p = ObjectMetadataLibrary.SerializableProperty( prop_name='test_int', prop_type=int, custom_names={} ) self.assertFalse(p.is_array) self.assertFalse(p.is_enum) self.assertFalse(p.is_optional) self.assertTrue(p.is_primitive_type()) def test_prop_optional_primitive_int_1(self) -> None: p = ObjectMetadataLibrary.SerializableProperty( prop_name='test_int', prop_type=Optional[int], custom_names={} ) self.assertFalse(p.is_array) self.assertFalse(p.is_enum) self.assertTrue(p.is_optional) self.assertTrue(p.is_primitive_type()) serializable-2.0.0/tests/test_oml_serializable_property.py000066400000000000000000000132411475213023200242130ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. import datetime from typing import List, Optional, Set from unittest import TestCase from py_serializable import ObjectMetadataLibrary from py_serializable.helpers import Iso8601Date from tests.model import BookEdition class TestOmlSerializableProperty(TestCase): def test_simple_primitive_1(self) -> None: sp = ObjectMetadataLibrary.SerializableProperty( prop_name='name', prop_type=str, custom_names={} ) self.assertEqual(sp.name, 'name') self.assertEqual(sp.type_, str) self.assertEqual(sp.concrete_type, str) self.assertDictEqual(sp.custom_names, {}) self.assertIsNone(sp.custom_type) self.assertFalse(sp.is_array) self.assertFalse(sp.is_enum) self.assertFalse(sp.is_optional) self.assertFalse(sp.is_xml_attribute) self.assertTrue(sp.is_primitive_type()) self.assertFalse(sp.is_helper_type()) def test_optional_simple_primitive_1(self) -> None: sp = ObjectMetadataLibrary.SerializableProperty( prop_name='name', prop_type=Optional[str], custom_names={} ) self.assertEqual(sp.name, 'name') self.assertEqual(sp.type_, Optional[str]) self.assertEqual(sp.concrete_type, str) self.assertDictEqual(sp.custom_names, {}) self.assertIsNone(sp.custom_type) self.assertFalse(sp.is_array) self.assertFalse(sp.is_enum) self.assertTrue(sp.is_optional) self.assertFalse(sp.is_xml_attribute) self.assertTrue(sp.is_primitive_type()) self.assertFalse(sp.is_helper_type()) def test_iterable_primitive_1(self) -> None: sp = ObjectMetadataLibrary.SerializableProperty( prop_name='name', prop_type=List[str], custom_names={} ) self.assertEqual(sp.name, 'name') self.assertEqual(sp.type_, List[str]) self.assertEqual(sp.concrete_type, str) self.assertDictEqual(sp.custom_names, {}) self.assertIsNone(sp.custom_type) self.assertTrue(sp.is_array) self.assertFalse(sp.is_enum) self.assertFalse(sp.is_optional) self.assertFalse(sp.is_xml_attribute) self.assertTrue(sp.is_primitive_type()) self.assertFalse(sp.is_helper_type()) def test_optional_iterable_primitive_1(self) -> None: sp = ObjectMetadataLibrary.SerializableProperty( prop_name='name', prop_type=Optional[List[str]], custom_names={} ) self.assertEqual(sp.name, 'name') self.assertEqual(sp.type_, Optional[List[str]]) self.assertEqual(sp.concrete_type, str) self.assertDictEqual(sp.custom_names, {}) self.assertIsNone(sp.custom_type) self.assertTrue(sp.is_array) self.assertFalse(sp.is_enum) self.assertTrue(sp.is_optional) self.assertFalse(sp.is_xml_attribute) self.assertTrue(sp.is_primitive_type()) self.assertFalse(sp.is_helper_type()) def test_sorted_set_1(self) -> None: sp = ObjectMetadataLibrary.SerializableProperty( prop_name='name', prop_type='SortedSet[BookEdition]', custom_names={} ) self.assertEqual(sp.name, 'name') self.assertEqual(sp.type_, Set[BookEdition]) self.assertEqual(sp.concrete_type, BookEdition) self.assertDictEqual(sp.custom_names, {}) self.assertIsNone(sp.custom_type) self.assertTrue(sp.is_array) self.assertFalse(sp.is_enum) self.assertFalse(sp.is_optional) self.assertFalse(sp.is_xml_attribute) self.assertFalse(sp.is_primitive_type()) self.assertFalse(sp.is_helper_type()) def test_sorted_set_2(self) -> None: sp = ObjectMetadataLibrary.SerializableProperty( prop_name='name', prop_type="SortedSet['BookEdition']", custom_names={} ) self.assertEqual(sp.name, 'name') self.assertEqual(sp.type_, Set[BookEdition]) self.assertEqual(sp.concrete_type, BookEdition) self.assertDictEqual(sp.custom_names, {}) self.assertIsNone(sp.custom_type) self.assertTrue(sp.is_array) self.assertFalse(sp.is_enum) self.assertFalse(sp.is_optional) self.assertFalse(sp.is_xml_attribute) self.assertFalse(sp.is_primitive_type()) self.assertFalse(sp.is_helper_type()) def test_datetime_using_helper(self) -> None: sp = ObjectMetadataLibrary.SerializableProperty( prop_name='publish_date', prop_type=datetime.datetime, custom_names={}, custom_type=Iso8601Date ) self.assertEqual(sp.name, 'publish_date') self.assertEqual(sp.type_, datetime.datetime) self.assertEqual(sp.concrete_type, datetime.datetime) self.assertDictEqual(sp.custom_names, {}) self.assertEqual(sp.custom_type, Iso8601Date) self.assertFalse(sp.is_array) self.assertFalse(sp.is_enum) self.assertFalse(sp.is_optional) self.assertFalse(sp.is_xml_attribute) self.assertFalse(sp.is_primitive_type()) self.assertTrue(sp.is_helper_type()) serializable-2.0.0/tests/test_xml.py000066400000000000000000000357071475213023200175450ustar00rootroot00000000000000# encoding: utf-8 # This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. import logging import os from copy import deepcopy from defusedxml import ElementTree as SafeElementTree from py_serializable.formatters import ( CamelCasePropertyNameFormatter, CurrentFormatter, KebabCasePropertyNameFormatter, SnakeCasePropertyNameFormatter, ) from tests.base import FIXTURES_DIRECTORY, BaseTestCase, DeepCompareMixin from tests.model import ( Book, SchemaVersion2, SchemaVersion3, SchemaVersion4, ThePhoenixProject, ThePhoenixProject_attr_serialized_none, ThePhoenixProject_unnormalized, ThePhoenixProject_v1, ) logger = logging.getLogger('py_serializable') logger.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') class TestXml(BaseTestCase, DeepCompareMixin): # region test_serialize def test_serialize_tfp_cc1(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-1.xml')) as expected_xml: self.assertEqualXml(expected_xml.read(), ThePhoenixProject.as_xml()) def test_serialize_tfp_cc1_v2(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-1-v2.xml')) as expected_xml: self.assertEqualXml(expected_xml.read(), ThePhoenixProject.as_xml(SchemaVersion2)) def test_serialize_tfp_cc1_v3(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-1-v3.xml')) as expected_xml: self.assertEqualXml(expected_xml.read(), ThePhoenixProject.as_xml(SchemaVersion3)) def test_serialize_tfp_cc1_v4(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-1-v4.xml')) as expected_xml: self.assertEqualXml(expected_xml.read(), ThePhoenixProject.as_xml(SchemaVersion4)) def test_serialize_tfp_kc1(self) -> None: CurrentFormatter.formatter = KebabCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-kebab-case-1.xml')) as expected_xml: self.assertEqualXml(expected_xml.read(), ThePhoenixProject.as_xml()) def test_serialize_tfp_kc1_v2(self) -> None: CurrentFormatter.formatter = KebabCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-kebab-case-1-v2.xml')) as expected_xml: self.assertEqualXml(expected_xml.read(), ThePhoenixProject.as_xml(SchemaVersion2)) def test_serialize_tfp_sc1(self) -> None: CurrentFormatter.formatter = SnakeCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-snake-case-1.xml'), 'r') as expected_xml: self.assertEqualXml(expected_xml.read(), ThePhoenixProject.as_xml()) def test_serializable_no_defaultNS(self) -> None: """regression test for https://github.com/madpah/serializable/issues/12""" from xml.etree import ElementTree xmlns = 'http://the.phoenix.project/testing/defaultNS' with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-defaultNS-unset.SNAPSHOT.xml')) as expected_xml: expected = expected_xml.read() data = deepcopy(ThePhoenixProject_v1) data._authors = {'Karl Ranseier', } # only one item, so order is no issue actual = ElementTree.tostring( data.as_xml(as_string=False, xmlns=xmlns), method='xml', encoding='unicode', # default_namespace=None ) # byte-wise string compare is intentional! self.maxDiff = None self.assertEqual(expected, actual) def test_serializable_with_defaultNS(self) -> None: """regression test for https://github.com/madpah/serializable/issues/12""" from xml.etree import ElementTree xmlns = 'http://the.phoenix.project/testing/defaultNS' with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-defaultNS-isset.SNAPSHOT.xml')) as expected_xml: expected = expected_xml.read() data = deepcopy(ThePhoenixProject_v1) data._authors = {'Karl Ranseier', } # only one item, so order is no issue actual = ElementTree.tostring( data.as_xml(SchemaVersion4, as_string=False, xmlns=xmlns), method='xml', encoding='unicode', default_namespace=xmlns, ) # byte-wise string compare is intentional! self.maxDiff = None self.assertEqual(expected, actual) def test_serialize_unnormalized(self) -> None: """regression test #119 for https://github.com/madpah/serializable/issues/114 and https://github.com/madpah/serializable/issues/115 """ CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-1-v4.xml')) as expected_xml: self.assertEqualXml(expected_xml.read(), ThePhoenixProject_unnormalized.as_xml(SchemaVersion4)) def test_serialize_attr_none(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-bookedition-none.xml')) as expected_xml: self.assertEqualXml(expected_xml.read(), ThePhoenixProject_attr_serialized_none.as_xml(SchemaVersion4)) # endregion test_serialize # region test_deserialize def test_deserialize_tfp_cc1(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-1-v1.xml')) as input_xml: book: Book = Book.from_xml(data=SafeElementTree.fromstring(input_xml.read())) self.assertEqual(ThePhoenixProject_v1.title, book.title) self.assertEqual(ThePhoenixProject_v1.isbn, book.isbn) self.assertEqual(ThePhoenixProject_v1.edition, book.edition) self.assertEqual(ThePhoenixProject_v1.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject_v1.publisher, book.publisher) self.assertEqual(ThePhoenixProject_v1.authors, book.authors) self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters) self.assertEqual(ThePhoenixProject_v1.rating, book.rating) def test_deserialize_tfp_cc1_v2(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-1-v2.xml')) as input_xml: book: Book = Book.from_xml(data=SafeElementTree.fromstring(input_xml.read())) self.assertEqual(ThePhoenixProject.title, book.title) self.assertEqual(ThePhoenixProject.isbn, book.isbn) self.assertEqual(ThePhoenixProject.edition, book.edition) self.assertEqual(ThePhoenixProject.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject.publisher.name, book.publisher.name) self.assertEqual(ThePhoenixProject.publisher.address, book.publisher.address) self.assertEqual(ThePhoenixProject.authors, book.authors) self.assertEqual(ThePhoenixProject.chapters, book.chapters) self.assertSetEqual(set(), book.references) self.assertEqual(ThePhoenixProject.rating, book.rating) def test_deserialize_tfp_cc1_v3(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-1-v3.xml')) as input_xml: book: Book = Book.from_xml(data=SafeElementTree.fromstring(input_xml.read())) self.assertEqual(ThePhoenixProject_v1.title, book.title) self.assertEqual(ThePhoenixProject_v1.isbn, book.isbn) self.assertEqual(ThePhoenixProject_v1.edition, book.edition) self.assertEqual(ThePhoenixProject_v1.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject_v1.publisher.name, book.publisher.name) self.assertEqual(ThePhoenixProject_v1.publisher.address, book.publisher.address) self.assertEqual(ThePhoenixProject_v1.authors, book.authors) self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters) self.assertEqual(ThePhoenixProject_v1.references, book.references) self.assertEqual(ThePhoenixProject_v1.rating, book.rating) def test_deserialize_tfp_cc1_v4(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-1-v4.xml')) as input_xml: book: Book = Book.from_xml(data=SafeElementTree.fromstring(input_xml.read())) self.assertEqual(ThePhoenixProject.title, book.title) self.assertEqual(ThePhoenixProject.isbn, book.isbn) self.assertEqual(ThePhoenixProject.edition, book.edition) self.assertEqual(ThePhoenixProject.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject.publisher.name, book.publisher.name) self.assertEqual(ThePhoenixProject.publisher.address, book.publisher.address) self.assertEqual(ThePhoenixProject.authors, book.authors) self.assertEqual(ThePhoenixProject.chapters, book.chapters) self.assertEqual(ThePhoenixProject.references, book.references) self.assertEqual(ThePhoenixProject.rating, book.rating) def test_deserialize_tfp_cc1_with_ignored(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-with-ignored.xml')) as input_xml: book: Book = Book.from_xml(data=SafeElementTree.fromstring(input_xml.read())) self.assertEqual(ThePhoenixProject_v1.title, book.title) self.assertEqual(ThePhoenixProject_v1.isbn, book.isbn) self.assertEqual(ThePhoenixProject_v1.edition, book.edition) self.assertEqual(ThePhoenixProject_v1.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject_v1.publisher, book.publisher) self.assertEqual(ThePhoenixProject_v1.authors, book.authors) self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters) self.assertEqual(ThePhoenixProject_v1.rating, book.rating) def test_deserialize_tfp_kc1(self) -> None: CurrentFormatter.formatter = KebabCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-kebab-case-1.xml')) as input_xml: book: Book = Book.from_xml(data=input_xml) self.assertEqual(ThePhoenixProject_v1.title, book.title) self.assertEqual(ThePhoenixProject_v1.isbn, book.isbn) self.assertEqual(ThePhoenixProject_v1.edition, book.edition) self.assertEqual(ThePhoenixProject_v1.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject_v1.publisher, book.publisher) self.assertEqual(ThePhoenixProject_v1.authors, book.authors) self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters) self.assertEqual(ThePhoenixProject_v1.rating, book.rating) def test_deserialize_tfp_sc1(self) -> None: CurrentFormatter.formatter = SnakeCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-snake-case-1.xml')) as input_xml: book: Book = Book.from_xml(data=SafeElementTree.fromstring(input_xml.read())) self.assertEqual(ThePhoenixProject_v1.title, book.title) self.assertEqual(ThePhoenixProject_v1.isbn, book.isbn) self.assertEqual(ThePhoenixProject_v1.edition, book.edition) self.assertEqual(ThePhoenixProject_v1.publish_date, book.publish_date) self.assertEqual(ThePhoenixProject_v1.publisher, book.publisher) self.assertEqual(ThePhoenixProject_v1.authors, book.authors) self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters) self.assertEqual(ThePhoenixProject_v1.rating, book.rating) def test_deserializable_with_defaultNS(self) -> None: """regression test for https://github.com/madpah/serializable/issues/11""" expected = ThePhoenixProject with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-defaultNS-isset-v4.xml')) as fixture_xml: actual = Book.from_xml(fixture_xml) self.assertDeepEqual(expected, actual) def test_deserializable_no_defaultNS_explicit(self) -> None: """regression test for https://github.com/madpah/serializable/issues/11""" expected = ThePhoenixProject with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-defaultNS-unset-v4.xml')) as fixture_xml: actual = Book.from_xml(fixture_xml, 'http://the.phoenix.project/testing/defaultNS') self.assertDeepEqual(expected, actual) def test_deserializable_no_defaultNS_autodetect(self) -> None: """regression test for https://github.com/madpah/serializable/issues/11""" expected = ThePhoenixProject with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-defaultNS-unset-v4.xml')) as fixture_xml: actual = Book.from_xml(fixture_xml) self.assertDeepEqual(expected, actual) def test_deserializable_mixed_defaultNS_autodetect(self) -> None: """regression test for https://github.com/madpah/serializable/issues/11""" expected = ThePhoenixProject with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-defaultNS-mixed-v4.xml')) as fixture_xml: actual = Book.from_xml(fixture_xml) self.assertDeepEqual(expected, actual) def test_deserializable_unnormalized(self) -> None: """regression test #119 for https://github.com/madpah/serializable/issues/114 and https://github.com/madpah/serializable/issues/115 """ expected = ThePhoenixProject with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project_unnormalized-input_v4.xml')) as fixture_xml: actual = Book.from_xml(fixture_xml) self.assertDeepEqual(expected, actual) # region test_deserialize serializable-2.0.0/tox.ini000066400000000000000000000032701475213023200154730ustar00rootroot00000000000000# This file is part of py-serializable # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. # tox (https://tox.readthedocs.io/) is a tool for running tests in multiple virtualenvs. # This configuration file will run the test suite on all supported python versions. # To use it, "pip install tox" and then run "tox" from this directory. [tox] minversion = 4.0 envlist = flake8 mypy-{current,lowest} py{312,311,310,39,38} skip_missing_interpreters = True usedevelop = False download = False [testenv] # settings in this category apply to all other testenv, if not overwritten skip_install = true allowlist_externals = poetry commands_pre = {envpython} --version poetry install -v poetry run pip freeze commands = poetry run coverage run --source=py_serializable -m unittest -v [testenv:mypy{,-current,-lowest}] skip_install = True commands = # mypy config is on own file: `.mypy.ini` !lowest: poetry run mypy lowest: poetry run mypy --python-version=3.8 [testenv:flake8] skip_install = True commands = # mypy config is in own file: `.flake8` poetry run flake8 py_serializable/ tests/