diff --git a/README.rst b/README.rst index d63d17a..39df072 100644 --- a/README.rst +++ b/README.rst @@ -39,8 +39,8 @@ The project has two goals: Supported Schema Validation Libraries ------------------------------------- -* `Schematics`_ >= 2.1.0 -* `Marshmallow`_ >= 2.15.1 +* `Schematics`_ >= 2.1.1 +* `Marshmallow`_ >= 4.2.0 Example diff --git a/dynamorm/exceptions.py b/dynamorm/exceptions.py index 4cb840e..c9ed086 100644 --- a/dynamorm/exceptions.py +++ b/dynamorm/exceptions.py @@ -1,7 +1,5 @@ import logging -import six - log = logging.getLogger(__name__) @@ -15,7 +13,6 @@ class DynaModelException(DynamoException): """Base exception for DynaModel problems""" -@six.python_2_unicode_compatible class ValidationError(DynaModelException): """Schema validation failed""" diff --git a/dynamorm/model.py b/dynamorm/model.py index 4f559ff..796fc00 100644 --- a/dynamorm/model.py +++ b/dynamorm/model.py @@ -6,8 +6,6 @@ import logging import sys -import six - from .exceptions import DynaModelException from .indexes import Index from .relationships import Relationship @@ -66,13 +64,13 @@ def should_transform(inner_class): # collect our indexes & relationships indexes = dict( (name, val) - for name, val in six.iteritems(attrs) + for name, val in attrs.items() if inspect.isclass(val) and issubclass(val, Index) ) attrs["relationships"] = dict( (name, val) - for name, val in six.iteritems(attrs) + for name, val in attrs.items() if isinstance(val, Relationship) ) @@ -119,11 +117,11 @@ def should_transform(inner_class): # Put the instantiated indexes back into our attrs. We instantiate the Index class that's in the attrs and # provide the actual Index object from our table as the parameter. - for name, klass in six.iteritems(indexes): + for name, klass in indexes.items(): index = klass(model, model.Table.indexes[klass.name]) setattr(model, name, index) - for relationship in six.itervalues(model.relationships): + for relationship in model.relationships.values(): relationship.set_this_model(model) model_prepared.send(model) @@ -131,8 +129,7 @@ def should_transform(inner_class): return model -@six.add_metaclass(DynaModelMeta) -class DynaModel(object): +class DynaModel(object, metaclass=DynaModelMeta): """``DynaModel`` is the base class all of your models will extend from. This model definition encapsulates the parameters used to create and manage the table as well as the schema for validating and marshalling data into object attributes. It will also hold any custom business logic you need for your objects. @@ -206,7 +203,7 @@ def __init__(self, partial=False, **raw): # from raw (since it would be ignored when validating anyway), and instead leverage the relationship to # determine if we should add any new values to raw to represent the relationship relationships = {} - for name, relationship in six.iteritems(self.relationships): + for name, relationship in self.relationships.items(): new_value = raw.pop(name, None) if new_value is not None: relationships[name] = new_value @@ -219,10 +216,10 @@ def __init__(self, partial=False, **raw): self._validated_data = self.Schema.dynamorm_validate( raw, partial=partial, native=True ) - for k, v in six.iteritems(self._validated_data): + for k, v in self._validated_data.items(): setattr(self, k, v) - for k, v in six.iteritems(relationships): + for k, v in relationships.items(): setattr(self, k, v) post_init.send(self.__class__, instance=self, partial=partial, raw=raw) @@ -298,9 +295,7 @@ def update_item(cls, conditions=None, update_item_kwargs=None, **kwargs): kwargs.update( dict( (k, v) - for k, v in six.iteritems( - cls.Schema.dynamorm_validate(kwargs, partial=True) - ) + for k, v in cls.Schema.dynamorm_validate(kwargs, partial=True).items() if k in kwargs ) ) @@ -473,7 +468,7 @@ def save(self, partial=False, unique=False, return_all=False, **kwargs): # TODO: Support the __ syntax to do deeply nested updates updates = dict( (k, getattr(self, k)) - for k, v in six.iteritems(self._validated_data) + for k, v in self._validated_data.items() if getattr(self, k) != v ) @@ -561,7 +556,7 @@ def update( # update our local attrs to match what we updated partial_model = self.new_from_raw(resp["Attributes"], partial=True) - for key, _ in six.iteritems(resp["Attributes"]): + for key, _ in resp["Attributes"].items(): # elsewhere in Dynamorm, models can be created without all fields (non-"strict" mode in Schematics), # so we drop unknown keys here to be consistent if hasattr(partial_model, key): diff --git a/dynamorm/relationships.py b/dynamorm/relationships.py index 12fd2f5..8813642 100644 --- a/dynamorm/relationships.py +++ b/dynamorm/relationships.py @@ -115,12 +115,9 @@ class Schema: ) """ -import six - from .signals import pre_save, post_save, pre_update, post_update -@six.python_2_unicode_compatible class DefaultBackReference(object): """When given a relationship the string representation of this will be a "python" string name of the model the relationship exists on. @@ -251,7 +248,7 @@ def __set__(self, obj, new_instance): raise TypeError("%s is not an instance of %s", new_instance, self.other) query = self.query(obj) - for key, val in six.iteritems(query): + for key, val in query.items(): setattr(new_instance, key, val) self.other_inst = new_instance diff --git a/dynamorm/table.py b/dynamorm/table.py index d6133c7..e5ee07d 100644 --- a/dynamorm/table.py +++ b/dynamorm/table.py @@ -45,15 +45,10 @@ import time import warnings from collections import defaultdict, OrderedDict - -try: - from collections.abc import Iterable, Mapping -except ImportError: - from collections import Iterable, Mapping +from collections.abc import Iterable, Mapping import boto3 import botocore -import six from boto3.dynamodb.conditions import Key, Attr from dynamorm.exceptions import ( @@ -195,7 +190,7 @@ def __init__(self, schema, indexes=None): self.indexes = {} if indexes: - for name, klass in six.iteritems(indexes): + for name, klass in indexes.items(): # Our indexes are just uninstantiated classes, but what we are interested in is what their parent class # name is. We can reach into the MRO to find that out, and then determine our own index type. index_type = klass.__mro__[1].__name__ @@ -207,7 +202,7 @@ def __init__(self, schema, indexes=None): name, (index_class,), dict( - (k, v) for k, v in six.iteritems(klass.__dict__) if k[0] != "_" + (k, v) for k, v in klass.__dict__.items() if k[0] != "_" ), ) @@ -243,7 +238,7 @@ def get_resource(cls, **kwargs): boto3_session = boto3.Session(**(cls.session_kwargs or {})) - for key, val in six.iteritems(cls.resource_kwargs or {}): + for key, val in (cls.resource_kwargs or {}).items(): kwargs.setdefault(key, val) # allow for dict based resource config that we convert into a botocore Config object @@ -301,7 +296,7 @@ def index_attribute_fields(self, index_name=None): """Return the attribute fields for a given index, or all indexes if omitted""" fields = set() - for index in six.itervalues(self.indexes): + for index in self.indexes.values(): if index_name and index.name != index_name: continue @@ -355,7 +350,7 @@ def create_table(self, wait=True): ) index_args = defaultdict(list) - for index in six.itervalues(self.indexes): + for index in self.indexes.values(): index_args[index.ARG_KEY].append(index.index_args) log.info("Creating table %s", self.name) @@ -462,7 +457,7 @@ def do_update(**kwargs): self.name, dict( (k, v) - for k, v in six.iteritems(table.provisioned_throughput) + for k, v in table.provisioned_throughput.items() if k.endswith("Units") ), self.provisioned_throughput, @@ -505,7 +500,7 @@ def do_update(**kwargs): existing_indexes[index["IndexName"]] = index - for index in six.itervalues(self.indexes): + for index in self.indexes.values(): if index.name in existing_indexes: current_capacity = existing_indexes[index.name]["ProvisionedThroughput"] if (index.read and index.write) and ( @@ -641,7 +636,7 @@ def get_update_expr_for_key(self, id_, parts): for part_id, part_name in enumerate(parts) ] ) - field_name = ".".join(six.iterkeys(field_expr_names)) + field_name = ".".join(field_expr_names.keys()) return ( UPDATE_FUNCTION_TEMPLATES[function].format( @@ -654,7 +649,7 @@ def get_update_expr_for_key(self, id_, parts): def update(self, update_item_kwargs=None, conditions=None, **kwargs): # copy update_item_kwargs, so that we don't mutate the original later on update_item_kwargs = dict( - (k, v) for k, v in six.iteritems(update_item_kwargs or {}) + (k, v) for k, v in (update_item_kwargs or {}).items() ) conditions = conditions or {} update_fields = [] @@ -722,12 +717,12 @@ def update(self, update_item_kwargs=None, conditions=None, **kwargs): def get_batch(self, keys, consistent=False, attrs=None, batch_get_kwargs=None): # copy batch_get_kwargs, so that we don't mutate the original later on batch_get_kwargs = dict( - (k, v) for k, v in six.iteritems(batch_get_kwargs or {}) + (k, v) for k, v in (batch_get_kwargs or {}).items() ) batch_get_kwargs["Keys"] = [] for kwargs in keys: - for k, v in six.iteritems(kwargs): + for k, v in kwargs.items(): if k not in self.schema.dynamorm_fields(): raise InvalidSchemaField( "{0} does not exist in the schema fields".format(k) @@ -757,9 +752,9 @@ def get_batch(self, keys, consistent=False, attrs=None, batch_get_kwargs=None): def get(self, consistent=False, get_item_kwargs=None, **kwargs): # copy get_item_kwargs, so that we don't mutate the original later on - get_item_kwargs = dict((k, v) for k, v in six.iteritems(get_item_kwargs or {})) + get_item_kwargs = dict((k, v) for k, v in (get_item_kwargs or {}).items()) - for k, v in six.iteritems(kwargs): + for k, v in kwargs.items(): if k not in self.schema.dynamorm_fields(): raise InvalidSchemaField( "{0} does not exist in the schema fields".format(k) @@ -777,7 +772,7 @@ def get(self, consistent=False, get_item_kwargs=None, **kwargs): def query(self, *args, **kwargs): # copy query_kwargs, so that we don't mutate the original later on query_kwargs = dict( - (k, v) for k, v in six.iteritems(kwargs.pop("query_kwargs", {})) + (k, v) for k, v in kwargs.pop("query_kwargs", {}).items() ) filter_kwargs = {} @@ -830,7 +825,7 @@ def query(self, *args, **kwargs): def scan(self, *args, **kwargs): # copy scan_kwargs, so that we don't mutate the original later on scan_kwargs = dict( - (k, v) for k, v in six.iteritems(kwargs.pop("scan_kwargs", {})) + (k, v) for k, v in kwargs.pop("scan_kwargs", {}).items() ) filter_expression = Q(**kwargs) @@ -856,7 +851,7 @@ def remove_nones(in_dict): try: return dict( (key, remove_nones(val)) - for key, val in six.iteritems(in_dict) + for key, val in in_dict.items() if val is not None ) except (ValueError, AttributeError): @@ -914,7 +909,7 @@ def Q(**mapping): return expression -class ReadIterator(six.Iterator): +class ReadIterator: """ReadIterator provides an iterator object that wraps a model and a method (either scan or query). Since it is an object we can attach attributes and functions to it that are useful to the caller. diff --git a/dynamorm/types/_marshmallow.py b/dynamorm/types/_marshmallow.py index da40a26..d48e961 100644 --- a/dynamorm/types/_marshmallow.py +++ b/dynamorm/types/_marshmallow.py @@ -1,7 +1,6 @@ -import six -from pkg_resources import parse_version +from packaging.version import Version -from marshmallow import Schema as MarshmallowSchema +from marshmallow import Schema as MarshmallowSchema, EXCLUDE from marshmallow.exceptions import MarshmallowError from marshmallow import fields, __version__ as marshmallow_version @@ -9,15 +8,15 @@ from ..exceptions import ValidationError # Define different validation logic depending on the version of marshmallow we're using -if parse_version(marshmallow_version) >= parse_version("3.0.0a1"): +if Version(marshmallow_version) >= Version("3.0.0a1"): def _validate(cls, obj, partial=False, native=False): """Validate using a Marshmallow v3+ schema""" try: if native: - data = cls().load(obj, partial=partial, unknown="EXCLUDE") + data = cls().load(obj, partial=partial, unknown=EXCLUDE) else: - data = cls(partial=partial, unknown="EXCLUDE").dump(obj) + data = cls(partial=partial, unknown=EXCLUDE).dump(obj) except MarshmallowError as e: raise ValidationError(obj, cls.__name__, e) return data @@ -60,7 +59,7 @@ def dynamorm_validate(cls, obj, partial=False, native=False): # When asking for partial native objects (during model init) we want to return None values # This ensures our object has all attributes and we can track partial saves properly if partial and native: - for name in six.iterkeys(cls().fields): + for name in cls().fields.keys(): if name not in data: data[name] = None diff --git a/setup.cfg b/setup.cfg index c5f7e3d..4361c01 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[wheel] -universal=1 - [aliases] test=pytest diff --git a/setup.py b/setup.py index 618a2e8..e5cf69c 100644 --- a/setup.py +++ b/setup.py @@ -5,27 +5,37 @@ setup( name="dynamorm", - version="0.11.0", + version="0.12.0", description="DynamORM is a Python object & relation mapping library for Amazon's DynamoDB service.", long_description=long_description, author="Evan Borgstrom", author_email="evan@borgstrom.ca", url="https://github.com/NerdWalletOSS/DynamORM", license="Apache License Version 2.0", - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4", - install_requires=["blinker>=1.4,<2.0", "boto3>=1.3,<2.0", "six"], + python_requires=">=3.11", + install_requires=["blinker>=1.9,<3.0", "boto3>=1.42.23,<2.0", "packaging>=25.0"], extras_require={ - "marshmallow": ["marshmallow>=2.15.1,<4"], - "schematics": ["schematics>=2.1.0,<3"], + "marshmallow": ["marshmallow>=3.0,<5.0"], + "schematics": ["schematics>=2.1.0,<3.0"], + "test": [ + "pytest>=7.0", + "pytest-mock>=3.0", + "python-dateutil>=2.8", + ], + "dev": [ + "pytest>=7.0", + "pytest-mock>=3.0", + "python-dateutil>=2.8", + "black>=22.0", + "tox>=4.0", + ], }, packages=["dynamorm", "dynamorm.types"], classifiers=[ "Development Status :: 4 - Beta", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", diff --git a/tox.ini b/tox.ini index b6d61e1..b7feb5e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,16 @@ [tox] envlist = black - py{2,3,py2,py3}-{schematics,marshmallow} + py{311,312,313}-{schematics,marshmallow} skipsdist = True skip_missing_interpreters = {env:TOX_SKIP_MISSING_INTERPRETERS:True} [testenv] basepython = - py2: python2 - py27: python2.7 - py3: python3 - py35: python3.5 - py36: python3.6 - py37: python3.7 - py38: python3.8 - pypy2: pypy - pypy3: pypy3 + py311: python3.11 + py312: python3.12 + py313: python3.13 deps = pytest