python-datamodel 0.6.18__tar.gz → 0.10.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_datamodel-0.10.1/CHANGELOG.md +17 -0
- python_datamodel-0.10.1/CONTRIBUTING.md +60 -0
- python_datamodel-0.10.1/MANIFEST.in +22 -0
- python_datamodel-0.10.1/Makefile +37 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/PKG-INFO +17 -12
- python_datamodel-0.10.1/SECURITY.md +18 -0
- python_datamodel-0.10.1/datamodel/abstract.py +383 -0
- python_datamodel-0.10.1/datamodel/adaptive/models.py +598 -0
- python_datamodel-0.10.1/datamodel/aliases/__init__.py +26 -0
- python_datamodel-0.10.1/datamodel/base.py +180 -0
- python_datamodel-0.10.1/datamodel/converters.c +43471 -0
- python_datamodel-0.10.1/datamodel/converters.html +17387 -0
- python_datamodel-0.10.1/datamodel/converters.pyx +1489 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/exceptions.c +1016 -651
- python_datamodel-0.10.1/datamodel/exceptions.html +1261 -0
- python_datamodel-0.10.1/datamodel/exceptions.pxd +13 -0
- python_datamodel-0.10.1/datamodel/exceptions.pyx +50 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/fields.cpp +1853 -1146
- python_datamodel-0.10.1/datamodel/fields.html +3912 -0
- python_datamodel-0.10.1/datamodel/fields.pyx +309 -0
- python-datamodel-0.6.18/datamodel/validation.cpp → python_datamodel-0.10.1/datamodel/functions.cpp +2771 -5918
- python_datamodel-0.10.1/datamodel/functions.html +1766 -0
- python_datamodel-0.10.1/datamodel/functions.pxd +9 -0
- python_datamodel-0.10.1/datamodel/functions.pyx +82 -0
- python_datamodel-0.10.1/datamodel/jsonld/__init__.py +45 -0
- python_datamodel-0.10.1/datamodel/jsonld/models.py +500 -0
- python_datamodel-0.10.1/datamodel/libs/__init__.py +1 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/libs/mapping.c +1441 -1179
- python_datamodel-0.10.1/datamodel/libs/mapping.html +2618 -0
- python_datamodel-0.10.1/datamodel/libs/mapping.pxd +11 -0
- python_datamodel-0.10.1/datamodel/libs/mapping.pyx +135 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/libs/mutables.py +19 -8
- python_datamodel-0.10.1/datamodel/models.py +814 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/parsers/encoders.py +1 -1
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/parsers/json.cpp +7176 -3159
- python_datamodel-0.10.1/datamodel/parsers/json.html +3365 -0
- python_datamodel-0.10.1/datamodel/parsers/json.pyx +250 -0
- python_datamodel-0.10.1/datamodel/rs_core/Cargo.toml +17 -0
- python_datamodel-0.10.1/datamodel/rs_core/src/lib.rs +294 -0
- python_datamodel-0.10.1/datamodel/rs_parsers/Cargo.toml +22 -0
- python_datamodel-0.10.1/datamodel/rs_parsers/src/lib.rs +571 -0
- python_datamodel-0.10.1/datamodel/rs_validators/Cargo.toml +17 -0
- python_datamodel-0.10.1/datamodel/typedefs/__init__.py +9 -0
- python_datamodel-0.10.1/datamodel/typedefs/singleton.c +9169 -0
- python_datamodel-0.10.1/datamodel/typedefs/singleton.html +629 -0
- python_datamodel-0.10.1/datamodel/typedefs/singleton.pxd +9 -0
- python_datamodel-0.10.1/datamodel/typedefs/singleton.pyx +24 -0
- python_datamodel-0.10.1/datamodel/typedefs/types.c +11716 -0
- python_datamodel-0.10.1/datamodel/typedefs/types.html +732 -0
- python_datamodel-0.10.1/datamodel/typedefs/types.pxd +11 -0
- python_datamodel-0.10.1/datamodel/typedefs/types.pyx +39 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/types.c +482 -465
- python_datamodel-0.10.1/datamodel/types.html +716 -0
- python_datamodel-0.10.1/datamodel/types.pyx +100 -0
- python_datamodel-0.10.1/datamodel/validation.cpp +17085 -0
- python_datamodel-0.10.1/datamodel/validation.html +4769 -0
- python_datamodel-0.10.1/datamodel/validation.pyx +315 -0
- python_datamodel-0.10.1/datamodel/version.py +13 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/pyproject.toml +24 -7
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/python_datamodel.egg-info/PKG-INFO +17 -12
- python_datamodel-0.10.1/python_datamodel.egg-info/SOURCES.txt +97 -0
- python_datamodel-0.10.1/python_datamodel.egg-info/not-zip-safe +1 -0
- python_datamodel-0.10.1/python_datamodel.egg-info/requires.txt +13 -0
- python_datamodel-0.10.1/python_datamodel.egg-info/top_level.txt +5 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/setup.cfg +1 -1
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/setup.py +75 -28
- python_datamodel-0.10.1/tests/__init__.py +0 -0
- python_datamodel-0.10.1/tests/test_aliases.py +78 -0
- python_datamodel-0.10.1/tests/test_converter.py +170 -0
- python_datamodel-0.10.1/tests/test_data.py +110 -0
- python_datamodel-0.10.1/tests/test_descriptors.py +88 -0
- python_datamodel-0.10.1/tests/test_dict.py +51 -0
- python_datamodel-0.10.1/tests/test_inherit.py +171 -0
- python_datamodel-0.10.1/tests/test_json.py +237 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/tests/test_method.py +2 -1
- python_datamodel-0.10.1/tests/test_primitives.py +146 -0
- python_datamodel-0.10.1/tests/test_qsdriver.py +184 -0
- python_datamodel-0.10.1/tests/test_qsmodel.py +219 -0
- python_datamodel-0.10.1/tests/test_ticket.py +157 -0
- python_datamodel-0.10.1/tests/test_tickets.py +228 -0
- python_datamodel-0.10.1/tests/test_tuples.py +47 -0
- python_datamodel-0.10.1/tests/test_type_user.py +62 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/tests/test_types.py +29 -21
- python_datamodel-0.10.1/tests/test_valid_callables.py +67 -0
- python_datamodel-0.10.1/tests/test_validations.py +251 -0
- python-datamodel-0.6.18/datamodel/abstract.py +0 -187
- python-datamodel-0.6.18/datamodel/base.py +0 -581
- python-datamodel-0.6.18/datamodel/converters.c +0 -25047
- python-datamodel-0.6.18/datamodel/models.py +0 -111
- python-datamodel-0.6.18/datamodel/version.py +0 -9
- python-datamodel-0.6.18/examples/basic.py +0 -152
- python-datamodel-0.6.18/examples/check.py +0 -91
- python-datamodel-0.6.18/examples/example.py +0 -190
- python-datamodel-0.6.18/examples/inherit.py +0 -40
- python-datamodel-0.6.18/examples/model.py +0 -63
- python-datamodel-0.6.18/examples/payroll.py +0 -113
- python-datamodel-0.6.18/examples/person.py +0 -49
- python-datamodel-0.6.18/examples/polymorph.py +0 -60
- python-datamodel-0.6.18/examples/schema.py +0 -68
- python-datamodel-0.6.18/examples/test_actor.py +0 -42
- python-datamodel-0.6.18/examples/test_model.py +0 -73
- python-datamodel-0.6.18/examples/test_nulls.py +0 -59
- python-datamodel-0.6.18/python_datamodel.egg-info/SOURCES.txt +0 -48
- python-datamodel-0.6.18/python_datamodel.egg-info/requires.txt +0 -12
- python-datamodel-0.6.18/python_datamodel.egg-info/top_level.txt +0 -1
- python-datamodel-0.6.18/tests/test_inherit.py +0 -47
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/LICENSE +0 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/README.md +0 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/__init__.py +0 -0
- {python-datamodel-0.6.18/datamodel/libs → python_datamodel-0.10.1/datamodel/adaptive}/__init__.py +0 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/parsers/__init__.py +0 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/profiler.py +0 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/datamodel/py.typed +0 -0
- /python-datamodel-0.6.18/examples/__init__.py → /python_datamodel-0.10.1/datamodel/rs_validators/src/lib.rs +0 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/python_datamodel.egg-info/dependency_links.txt +0 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/tests/test_classdict.py +0 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/tests/test_field.py +0 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/tests/test_model.py +0 -0
- {python-datamodel-0.6.18 → python_datamodel-0.10.1}/tests/test_qsobject.py +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this
|
|
6
|
+
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
## [0.0.15] - 2022-09-15
|
|
8
|
+
* fixing building wheel for x86_64
|
|
9
|
+
* fixing behaviors over Meta class in Models with missing attributes
|
|
10
|
+
|
|
11
|
+
## [0.0.7] - 2022-09-14
|
|
12
|
+
* Added "from_dict" and "from_json" methods to create datamodels from json strings and dictionaries
|
|
13
|
+
* added a new json encoder, based on orjson
|
|
14
|
+
* "model()" method export a json version of Model (serialization).
|
|
15
|
+
|
|
16
|
+
## [0.0.1] - 2022-09-12
|
|
17
|
+
* First version
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Contributing to DataModel
|
|
2
|
+
|
|
3
|
+
## Preparation
|
|
4
|
+
|
|
5
|
+
DataModel is designed to use last syntax of asyncio-tools, for that reason, you'll need to have at least Python 3.8 available for testing.
|
|
6
|
+
|
|
7
|
+
You can do this with [pyenv][]:
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
$ pyenv install 3.8.1
|
|
11
|
+
$ pyenv local 3.8.1
|
|
12
|
+
|
|
13
|
+
Or using virtualenv:
|
|
14
|
+
|
|
15
|
+
$ python3.8 -m venv .venv
|
|
16
|
+
|
|
17
|
+
Also, we can use the command "make venv" inside of Makefile.
|
|
18
|
+
|
|
19
|
+
$ make venv
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
Once cloned, create a clean virtual environment and
|
|
24
|
+
install the appropriate tools and dependencies:
|
|
25
|
+
|
|
26
|
+
$ cd <path/to/DataModel>
|
|
27
|
+
$ make venv
|
|
28
|
+
$ source .venv/bin/activate
|
|
29
|
+
$ make setup
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## Formatting
|
|
33
|
+
|
|
34
|
+
DataModel start using *[black][]* for formating code.
|
|
35
|
+
|
|
36
|
+
$ make format
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Testing
|
|
40
|
+
|
|
41
|
+
Once you've made changes, you should run unit tests,
|
|
42
|
+
validate your type annotations, and ensure your code
|
|
43
|
+
meets the appropriate style and linting rules:
|
|
44
|
+
|
|
45
|
+
$ make test lint
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Submitting
|
|
49
|
+
|
|
50
|
+
Before submitting a pull request, please ensure
|
|
51
|
+
that you have done the following:
|
|
52
|
+
|
|
53
|
+
* Documented changes or features in README.md
|
|
54
|
+
* Added appropriate license headers to new files
|
|
55
|
+
* Written or modified tests for new functionality
|
|
56
|
+
* Formatted code following project standards
|
|
57
|
+
* Validated code and formatting with `make test lint`
|
|
58
|
+
|
|
59
|
+
[black]: https://github.com/psf/black
|
|
60
|
+
[pyenv]: https://github.com/pyenv/pyenv
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include CHANGELOG.md
|
|
3
|
+
include CONTRIBUTING.md
|
|
4
|
+
include SECURITY.md
|
|
5
|
+
include README.md
|
|
6
|
+
include Makefile
|
|
7
|
+
|
|
8
|
+
graft datamodel
|
|
9
|
+
graft tests
|
|
10
|
+
|
|
11
|
+
recursive-include datamodel *.pxd *.pyx
|
|
12
|
+
recursive-include datamodel/rs_parsers *
|
|
13
|
+
|
|
14
|
+
# Exclude tests, settings, env, examples, and bin folders
|
|
15
|
+
global-exclude *.pyc
|
|
16
|
+
prune docs
|
|
17
|
+
prune settings
|
|
18
|
+
prune env
|
|
19
|
+
prune examples
|
|
20
|
+
prune bin
|
|
21
|
+
recursive-exclude */__pycache__
|
|
22
|
+
prune */__pycache__
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
venv:
|
|
2
|
+
python3.11 -m venv .venv
|
|
3
|
+
echo 'run `source .venv/bin/activate` to start develop DataModel'
|
|
4
|
+
|
|
5
|
+
venv12:
|
|
6
|
+
python3.12 -m venv .venv11
|
|
7
|
+
echo 'run `source .venv/bin/activate` to start develop DataModel'
|
|
8
|
+
|
|
9
|
+
install:
|
|
10
|
+
pip install -e .
|
|
11
|
+
|
|
12
|
+
develop:
|
|
13
|
+
pip install -e .
|
|
14
|
+
pip install -Ur docs/requirements-dev.txt
|
|
15
|
+
|
|
16
|
+
compile:
|
|
17
|
+
python setup.py build_ext --inplace
|
|
18
|
+
|
|
19
|
+
release:
|
|
20
|
+
lint test clean
|
|
21
|
+
flit publish
|
|
22
|
+
|
|
23
|
+
format:
|
|
24
|
+
python -m black datamodel
|
|
25
|
+
|
|
26
|
+
lint:
|
|
27
|
+
python -m pylint --rcfile .pylintrc datamodels/*.py
|
|
28
|
+
python -m pylint --rcfile .pylintrc datamodels/models/*.py
|
|
29
|
+
python -m black --check datamodels
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
python -m coverage run -m datamodels.tests
|
|
33
|
+
python -m coverage report
|
|
34
|
+
python -m mypy datamodels/*.py
|
|
35
|
+
|
|
36
|
+
distclean:
|
|
37
|
+
rm -rf .venv
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-datamodel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
4
4
|
Summary: simple library based on python +3.8 to use Dataclass-syntaxfor interacting with Data
|
|
5
5
|
Home-page: https://github.com/phenobarbital/python-datamodel
|
|
6
6
|
Author: Jesus Lara
|
|
7
7
|
Author-email: jesuslarag@gmail.com
|
|
8
8
|
License: BSD
|
|
9
|
-
Project-URL: Source, https://github.com/phenobarbital/
|
|
9
|
+
Project-URL: Source, https://github.com/phenobarbital/datamodel
|
|
10
10
|
Project-URL: Funding, https://paypal.me/phenobarbital
|
|
11
|
+
Project-URL: Tracker, https://github.com/phenobarbital/datamodel/issues
|
|
12
|
+
Project-URL: Documentation, https://datamodel.readthedocs.io/en/latest/
|
|
13
|
+
Project-URL: Buy Me A Coffee!, https://www.buymeacoffee.com/phenobarbital
|
|
11
14
|
Project-URL: Say Thanks!, https://saythanks.io/to/phenobarbital
|
|
12
15
|
Keywords: asyncio,dataclass,dataclasses,data models
|
|
13
16
|
Platform: any
|
|
@@ -18,30 +21,32 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
|
18
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
22
|
Classifier: Programming Language :: Python :: 3
|
|
20
23
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
22
24
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
25
|
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
28
|
Classifier: Framework :: AsyncIO
|
|
25
29
|
Classifier: License :: OSI Approved :: BSD License
|
|
26
30
|
Classifier: Operating System :: OS Independent
|
|
27
31
|
Classifier: Topic :: System :: Systems Administration
|
|
28
32
|
Classifier: Topic :: Utilities
|
|
29
33
|
Classifier: Environment :: Web Environment
|
|
30
|
-
Requires-Python: >=3.
|
|
34
|
+
Requires-Python: >=3.10.0
|
|
31
35
|
Description-Content-Type: text/markdown
|
|
32
36
|
License-File: LICENSE
|
|
33
|
-
Requires-Dist: numpy
|
|
34
|
-
Requires-Dist: uvloop
|
|
37
|
+
Requires-Dist: numpy>=1.26.4
|
|
38
|
+
Requires-Dist: uvloop>=0.21.0
|
|
35
39
|
Requires-Dist: asyncio==3.4.3
|
|
36
40
|
Requires-Dist: faust-cchardet==2.1.19
|
|
37
|
-
Requires-Dist: ciso8601==2.3.
|
|
41
|
+
Requires-Dist: ciso8601==2.3.2
|
|
38
42
|
Requires-Dist: objectpath==0.6.1
|
|
39
|
-
Requires-Dist: orjson
|
|
40
|
-
Requires-Dist: typing_extensions
|
|
41
|
-
Requires-Dist: asyncpg
|
|
42
|
-
Requires-Dist: python-dateutil
|
|
43
|
-
Requires-Dist: pendulum==2.1.2
|
|
43
|
+
Requires-Dist: orjson>=3.10.11
|
|
44
|
+
Requires-Dist: typing_extensions>=4.9.0
|
|
45
|
+
Requires-Dist: asyncpg>=0.29.0
|
|
46
|
+
Requires-Dist: python-dateutil>=2.8.2
|
|
44
47
|
Requires-Dist: python-slugify==8.0.1
|
|
48
|
+
Requires-Dist: psycopg2-binary==2.9.10
|
|
49
|
+
Requires-Dist: msgspec==0.19.0
|
|
45
50
|
|
|
46
51
|
# DataModel
|
|
47
52
|
DataModel is a simple library based on python +3.8 to use Dataclass-syntax for interacting with
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
We release patches for security vulnerabilities. Which versions are eligible
|
|
6
|
+
receiving such patches depend on the CVSS v3.0 Rating:
|
|
7
|
+
|
|
8
|
+
| CVSS v3.0 | Supported Versions |
|
|
9
|
+
| --------- | ----------------------------------------- |
|
|
10
|
+
| 9.0-10.0 | Releases within the previous three months |
|
|
11
|
+
| 4.0-8.9 | Most recent release |
|
|
12
|
+
|
|
13
|
+
## Reporting a Vulnerability
|
|
14
|
+
|
|
15
|
+
Please report (suspected) security vulnerabilities to
|
|
16
|
+
**[jesularag@gmail.com](mailto:jesularag@gmail.com)**. You will receive a response from
|
|
17
|
+
us within 48 hours. If the issue is confirmed, we will release a patch as soon
|
|
18
|
+
as possible depending on complexity but historically within a few days.
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional, Any, List, Dict, get_args, get_origin, ClassVar
|
|
4
|
+
from types import GenericAlias
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
import types
|
|
8
|
+
from inspect import isclass
|
|
9
|
+
from dataclasses import dataclass, InitVar
|
|
10
|
+
from .parsers.json import JSONContent
|
|
11
|
+
from .converters import encoders, parse_basic
|
|
12
|
+
from .validation import validators
|
|
13
|
+
from .fields import Field
|
|
14
|
+
from .functions import (
|
|
15
|
+
is_dataclass,
|
|
16
|
+
is_primitive
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
class Meta:
|
|
20
|
+
"""
|
|
21
|
+
Metadata information about Model.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
name: str = "" name of the model
|
|
25
|
+
description: str = "" description of the model
|
|
26
|
+
schema: str = "" schema of the model (optional)
|
|
27
|
+
frozen: bool = False if the model (dataclass) is read-only (frozen state)
|
|
28
|
+
strict: bool = True if the model (dataclass) should raise an error on invalid data.
|
|
29
|
+
remove_null: bool = True if the model should remove null values from the data.
|
|
30
|
+
validate_assignment: bool = True if the model should validate during assignment.
|
|
31
|
+
"""
|
|
32
|
+
name: str = ""
|
|
33
|
+
description: str = ""
|
|
34
|
+
schema: str = ""
|
|
35
|
+
frozen: bool = False
|
|
36
|
+
strict: bool = True
|
|
37
|
+
driver: str = None
|
|
38
|
+
credentials: dict = Optional[dict]
|
|
39
|
+
dsn: Optional[str] = None
|
|
40
|
+
datasource: Optional[str] = None
|
|
41
|
+
connection: Optional[Callable] = None
|
|
42
|
+
remove_nulls: bool = False
|
|
43
|
+
endpoint: str
|
|
44
|
+
extra: str = 'forbid' # could be 'allow', 'ignore', or 'forbid'
|
|
45
|
+
validate_assignment: bool = False
|
|
46
|
+
as_objects: bool = False
|
|
47
|
+
no_nesting: bool = False
|
|
48
|
+
alias_function: Optional[Callable] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def set_connection(cls, conn: Callable):
|
|
52
|
+
cls.connection = conn
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _dc_method_setattr_(self, name: str, value: Any) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Simplified __setattr__ for dataclass-like objects.
|
|
58
|
+
|
|
59
|
+
This version separates the known-field assignment (with optional validation)
|
|
60
|
+
from the “extra field” assignment and uses a helper to perform conversion/validation.
|
|
61
|
+
"""
|
|
62
|
+
# Ensure that the __values__ dict is present.
|
|
63
|
+
if not hasattr(self, '__values__'):
|
|
64
|
+
object.__setattr__(self, '__values__', {})
|
|
65
|
+
|
|
66
|
+
# Check whether we are assigning to a known field.
|
|
67
|
+
if name in self.__fields__:
|
|
68
|
+
# Save the initial value (only once).
|
|
69
|
+
self.__values__.setdefault(name, value)
|
|
70
|
+
|
|
71
|
+
# If assignment validation is active, convert the value.
|
|
72
|
+
if self.Meta.validate_assignment:
|
|
73
|
+
value = _validate_field_assignment(self, name, value)
|
|
74
|
+
object.__setattr__(self, name, value)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# If the class is frozen, do not allow new attributes.
|
|
78
|
+
if self.Meta.frozen:
|
|
79
|
+
raise TypeError(
|
|
80
|
+
f"Cannot add new attribute {name!r} on {self.modelName} "
|
|
81
|
+
"(the class is frozen)"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# For extra attributes, store them as usual.
|
|
85
|
+
# (Note: here we “neutralize” any callable value to None if needed.)
|
|
86
|
+
object.__setattr__(self, name, None if callable(value) else value)
|
|
87
|
+
if name == '__values__':
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# If the field isn’t known yet:
|
|
91
|
+
if name not in self.__fields__:
|
|
92
|
+
# In strict mode, we don’t allow unknown fields.
|
|
93
|
+
if self.Meta.strict:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
# Otherwise, check the "extra" policy.
|
|
97
|
+
extra_policy = self.Meta.extra
|
|
98
|
+
if extra_policy == 'forbid':
|
|
99
|
+
raise TypeError(f"Field {name!r} is not allowed on {self.modelName}")
|
|
100
|
+
elif extra_policy == 'ignore':
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Dynamically create a new Field for the unknown attribute.
|
|
104
|
+
try:
|
|
105
|
+
new_field = Field(required=False, default=value)
|
|
106
|
+
new_field.name = name
|
|
107
|
+
new_field.type = type(value)
|
|
108
|
+
# (Optionally, you might attach a parser here if validation is on.)
|
|
109
|
+
self.__columns__[name] = new_field
|
|
110
|
+
self.__fields__.append(name)
|
|
111
|
+
object.__setattr__(self, name, value)
|
|
112
|
+
except Exception as err:
|
|
113
|
+
logging.exception(err, stack_info=True)
|
|
114
|
+
raise
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _validate_field_assignment(self, name: str, value: Any) -> Any:
|
|
118
|
+
"""
|
|
119
|
+
Helper that applies field conversion/validation based on cached field info.
|
|
120
|
+
|
|
121
|
+
If you cache the parser (or the type-category) on the Field during model creation,
|
|
122
|
+
this helper could simply call that parser.
|
|
123
|
+
"""
|
|
124
|
+
field_obj = self.__columns__[name]
|
|
125
|
+
# _type = field_obj.type
|
|
126
|
+
# _encoder = field_obj.metadata.get('encoder')
|
|
127
|
+
# Retrieve the field category (pre‐computed at class creation)
|
|
128
|
+
# field_category = self.__field_types__.get(name, 'complex')
|
|
129
|
+
try:
|
|
130
|
+
return field_obj.parser(value) if field_obj.parser else value
|
|
131
|
+
except Exception as e:
|
|
132
|
+
raise TypeError(
|
|
133
|
+
f"Cannot assign {value!r} to field {name!r}: {e}"
|
|
134
|
+
) from e
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ModelMeta(type):
|
|
138
|
+
"""ModelMeta.
|
|
139
|
+
|
|
140
|
+
Metaclass for DataModels, convert any Model into a dataclass-compatible object.
|
|
141
|
+
"""
|
|
142
|
+
__columns__: Dict
|
|
143
|
+
__fields__: List
|
|
144
|
+
__field_types__: List
|
|
145
|
+
__aliases__: Dict
|
|
146
|
+
|
|
147
|
+
def __new__(cls, name, bases, attrs, **kwargs): # noqa
|
|
148
|
+
cols = OrderedDict()
|
|
149
|
+
strict = False
|
|
150
|
+
cls.__field_types__ = {}
|
|
151
|
+
cls.__typing_args__ = {}
|
|
152
|
+
cls.__aliases__ = {}
|
|
153
|
+
_types = {}
|
|
154
|
+
_typing_args = {}
|
|
155
|
+
aliases = {}
|
|
156
|
+
|
|
157
|
+
if "__annotations__" in attrs:
|
|
158
|
+
annotations = attrs.get('__annotations__', {})
|
|
159
|
+
with contextlib.suppress(TypeError, AttributeError, KeyError):
|
|
160
|
+
strict = attrs['Meta'].strict
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _initialize_fields(attrs, annotations, strict):
|
|
164
|
+
cols = OrderedDict()
|
|
165
|
+
_types_local = {}
|
|
166
|
+
_typing_args = {}
|
|
167
|
+
aliases = {}
|
|
168
|
+
for field, _type in annotations.items():
|
|
169
|
+
if isinstance(_type, InitVar) or _type == InitVar:
|
|
170
|
+
# Skip InitVar fields;
|
|
171
|
+
# they should not be part of the dataclass instance
|
|
172
|
+
continue
|
|
173
|
+
origin = get_origin(_type)
|
|
174
|
+
if origin is ClassVar:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
# Check if the field's default value is a descriptor
|
|
178
|
+
default_value = attrs.get(field, None)
|
|
179
|
+
is_descriptor = any(
|
|
180
|
+
hasattr(default_value, method)
|
|
181
|
+
for method in ("__get__", "__set__", "__delete__")
|
|
182
|
+
)
|
|
183
|
+
# Handle the descriptor field
|
|
184
|
+
if is_descriptor:
|
|
185
|
+
default_value._type_category = 'descriptor'
|
|
186
|
+
cols[field] = default_value
|
|
187
|
+
_types_local[field] = 'descriptor'
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
if isinstance(_type, Field):
|
|
191
|
+
_type = _type.type
|
|
192
|
+
df = attrs.get(
|
|
193
|
+
field,
|
|
194
|
+
Field(type=_type, required=False, default=None)
|
|
195
|
+
)
|
|
196
|
+
if df is not None and isinstance(df, Field):
|
|
197
|
+
alias = df.metadata.get("alias", None)
|
|
198
|
+
if alias:
|
|
199
|
+
aliases[alias] = field
|
|
200
|
+
if not isinstance(df, Field):
|
|
201
|
+
df = Field(required=False, type=_type, default=df)
|
|
202
|
+
df.name = field
|
|
203
|
+
df.type = _type
|
|
204
|
+
|
|
205
|
+
# Cache reflection info so we DON’T need to call
|
|
206
|
+
# get_origin/get_args repeatedly:
|
|
207
|
+
args = get_args(_type)
|
|
208
|
+
_default = df.default
|
|
209
|
+
_is_dc = is_dataclass(_type)
|
|
210
|
+
_is_prim = is_primitive(_type)
|
|
211
|
+
_is_alias = isinstance(_type, GenericAlias)
|
|
212
|
+
_is_typing = hasattr(_type, '__module__') and _type.__module__ == 'typing' # noqa
|
|
213
|
+
|
|
214
|
+
# Store the type info in the field object:
|
|
215
|
+
df.is_dc = _is_dc
|
|
216
|
+
df.is_primitive = _is_prim
|
|
217
|
+
df.is_typing = _is_typing
|
|
218
|
+
df.origin = origin
|
|
219
|
+
df.args = args
|
|
220
|
+
df.type_args = getattr(_type, '__args__', None)
|
|
221
|
+
|
|
222
|
+
df._typeinfo_ = {
|
|
223
|
+
"default_callable": callable(_default)
|
|
224
|
+
}
|
|
225
|
+
# Current Field have an Encoder Function.
|
|
226
|
+
custom_encoder = df.metadata.get("encoder")
|
|
227
|
+
try:
|
|
228
|
+
df.parser = encoders[_type]
|
|
229
|
+
except (TypeError, KeyError):
|
|
230
|
+
df.parser = None
|
|
231
|
+
if custom_encoder:
|
|
232
|
+
df.parser = lambda value, _type=_type, encoder=custom_encoder: parse_basic(_type, value, encoder) # noqa
|
|
233
|
+
# Caching Validator:
|
|
234
|
+
try:
|
|
235
|
+
df.validator = validators[_type]
|
|
236
|
+
except (KeyError, TypeError):
|
|
237
|
+
df.validator = None
|
|
238
|
+
|
|
239
|
+
# check type of field:
|
|
240
|
+
if _is_prim:
|
|
241
|
+
_type_category = 'primitive'
|
|
242
|
+
elif origin == type:
|
|
243
|
+
_type_category = 'type'
|
|
244
|
+
elif _is_dc:
|
|
245
|
+
_type_category = 'dataclass'
|
|
246
|
+
elif _is_typing or _is_alias: # noqa
|
|
247
|
+
if df.origin is not None and (df.origin is list and df.args):
|
|
248
|
+
df._inner_type = args[0]
|
|
249
|
+
df._inner_origin = get_origin(df._inner_type)
|
|
250
|
+
df._typing_args = get_args(df._inner_type)
|
|
251
|
+
df._inner_is_dc = is_dataclass(df._inner_type)
|
|
252
|
+
try:
|
|
253
|
+
df._encoder_fn = encoders[df._inner_type]
|
|
254
|
+
except (TypeError, KeyError):
|
|
255
|
+
df._encoder_fn = None
|
|
256
|
+
if origin is list:
|
|
257
|
+
inner_type = args[0]
|
|
258
|
+
try:
|
|
259
|
+
df._encoder_fn = encoders[inner_type]
|
|
260
|
+
except (TypeError, KeyError):
|
|
261
|
+
df._encoder_fn = None
|
|
262
|
+
_type_category = 'typing'
|
|
263
|
+
elif isclass(_type):
|
|
264
|
+
_type_category = 'class'
|
|
265
|
+
# elif _is_alias:
|
|
266
|
+
# _type_category = 'typing'
|
|
267
|
+
else:
|
|
268
|
+
# TODO: making parser for complex types
|
|
269
|
+
_type_category = 'complex'
|
|
270
|
+
_types_local[field] = _type_category
|
|
271
|
+
df._type_category = _type_category
|
|
272
|
+
|
|
273
|
+
# Store them in a dict keyed by field name:
|
|
274
|
+
_typing_args[field] = (origin, args)
|
|
275
|
+
# Assign the field object to the attrs so dataclass can pick it up
|
|
276
|
+
attrs[field] = df
|
|
277
|
+
cols[field] = df
|
|
278
|
+
return cols, _types_local, _typing_args, aliases
|
|
279
|
+
|
|
280
|
+
# Initialize the fields
|
|
281
|
+
cols, _types, _typing_args, aliases = _initialize_fields(
|
|
282
|
+
attrs, annotations, strict
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
# if no __annotations__, cols is empty:
|
|
286
|
+
cols = OrderedDict()
|
|
287
|
+
|
|
288
|
+
_columns = cols.keys()
|
|
289
|
+
cls.__slots__ = tuple(_columns)
|
|
290
|
+
|
|
291
|
+
# Pop Meta before creating the class so we can assign it after
|
|
292
|
+
attr_meta = attrs.pop("Meta", None)
|
|
293
|
+
# Create the class
|
|
294
|
+
new_cls = super().__new__(cls, name, bases, attrs, **kwargs)
|
|
295
|
+
|
|
296
|
+
# Attach Meta class
|
|
297
|
+
new_cls.Meta = attr_meta or getattr(new_cls, "Meta", Meta)
|
|
298
|
+
new_cls.__dataclass_fields__ = cols
|
|
299
|
+
new_cls.__typing_args__ = _typing_args
|
|
300
|
+
if not new_cls.Meta:
|
|
301
|
+
new_cls.Meta = Meta
|
|
302
|
+
new_cls.Meta.set_connection = types.MethodType(
|
|
303
|
+
set_connection, new_cls.Meta
|
|
304
|
+
)
|
|
305
|
+
try:
|
|
306
|
+
frozen = new_cls.Meta.frozen
|
|
307
|
+
except AttributeError:
|
|
308
|
+
new_cls.Meta.frozen = False
|
|
309
|
+
frozen = False
|
|
310
|
+
|
|
311
|
+
# mix values from Meta to an existing Meta Class
|
|
312
|
+
new_cls.Meta.__annotations__ = Meta.__annotations__
|
|
313
|
+
for key, _ in Meta.__annotations__.items():
|
|
314
|
+
if not hasattr(new_cls.Meta, key):
|
|
315
|
+
try:
|
|
316
|
+
setattr(new_cls.Meta, key, None)
|
|
317
|
+
except AttributeError as e:
|
|
318
|
+
logging.warning(
|
|
319
|
+
f'Missing Meta Key: {key}, {e}'
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# If there's a __model_init__ method, call it
|
|
323
|
+
try:
|
|
324
|
+
new_cls.__model_init__(
|
|
325
|
+
new_cls,
|
|
326
|
+
name,
|
|
327
|
+
attrs
|
|
328
|
+
)
|
|
329
|
+
except AttributeError:
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
# Now that fields are in attrs, decorate the class as a dataclass
|
|
333
|
+
dc = dataclass(
|
|
334
|
+
unsafe_hash=strict,
|
|
335
|
+
repr=False,
|
|
336
|
+
init=True,
|
|
337
|
+
order=False,
|
|
338
|
+
eq=True,
|
|
339
|
+
frozen=frozen
|
|
340
|
+
)(new_cls)
|
|
341
|
+
# Set additional attributes:
|
|
342
|
+
dc.__columns__ = cols
|
|
343
|
+
dc.__fields__ = list(_columns)
|
|
344
|
+
dc.__values__ = {}
|
|
345
|
+
dc.__encoder__ = JSONContent
|
|
346
|
+
dc.__valid__ = False
|
|
347
|
+
dc.__errors__ = None
|
|
348
|
+
dc.__frozen__ = strict
|
|
349
|
+
dc.__initialised__ = False
|
|
350
|
+
dc.__field_types__ = _types
|
|
351
|
+
dc.__aliases__ = aliases
|
|
352
|
+
dc.__typing_args__ = _typing_args
|
|
353
|
+
dc.modelName = dc.__name__
|
|
354
|
+
|
|
355
|
+
# Override __setattr__ method
|
|
356
|
+
setattr(dc, "__setattr__", _dc_method_setattr_)
|
|
357
|
+
return dc
|
|
358
|
+
|
|
359
|
+
def __init__(cls, *args, **kwargs) -> None:
|
|
360
|
+
# Initialized Data Model = True
|
|
361
|
+
cls.__initialised__ = True
|
|
362
|
+
cls.__errors__ = None
|
|
363
|
+
super().__init__(*args, **kwargs)
|
|
364
|
+
|
|
365
|
+
def __call__(cls, *args, **kwargs):
|
|
366
|
+
# rename any kwargs that match an alias ONLY if there are aliases defined.
|
|
367
|
+
alias_func = getattr(cls.Meta, "alias_function", None)
|
|
368
|
+
if callable(alias_func):
|
|
369
|
+
new_kwargs = {}
|
|
370
|
+
for k, v in kwargs.items():
|
|
371
|
+
new_k = alias_func(k)
|
|
372
|
+
new_kwargs[new_k] = v
|
|
373
|
+
kwargs = new_kwargs
|
|
374
|
+
if cls.__aliases__:
|
|
375
|
+
new_kwargs = {}
|
|
376
|
+
for k, v in kwargs.items():
|
|
377
|
+
if k in cls.__aliases__:
|
|
378
|
+
real_field = cls.__aliases__[k]
|
|
379
|
+
new_kwargs[real_field] = v
|
|
380
|
+
else:
|
|
381
|
+
new_kwargs[k] = v
|
|
382
|
+
kwargs = new_kwargs
|
|
383
|
+
return super().__call__(*args, **kwargs)
|