# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['pydantic_dynamo']

package_data = \
{'': ['*']}

install_requires = \
['boto3>=1.26.85,<2.0.0', 'pydantic>=1.10.5,<2.0.0']

setup_kwargs = {
    'name': 'pydantic-dynamo',
    'version': '0.1.4',
    'description': '',
    'long_description': '# pydantic-dynamo\nA Python repository over DynamoDB leveraging the excellent \n[Pydantic](https://docs.pydantic.dev/) library to model records.\n\n## Installation\nInstall from PyPI `pip install pydantic-dynamo`\n\nOr even better, use [Poetry](https://python-poetry.org/docs/) `poetry add pydantic-dynamo`\n\n## Usage\n\nThe intended usage of this package is a low-to-medium complexity application. You will lose\nsome benefits of the single-table pattern, specifically the ability to query\nand retrieve objects of different types in one connection. For most use cases except\nthe most complex examples, access patterns can be implemented to utilize the `list`/`list_between`\nand `get_batch` functions, documented below, to prevent N+1 queries.\n\nThis package assumes the specified table already exists and the application it is\nrunning from has sufficient access to the table.\n\nCurrently, the package requires the table be created with a partition key named \n`_table_item_id` and a sort key named `_table_content_id`.\n\nThe following IAM permissions are required:\n\n```yaml\n- dynamodb:BatchGetItem\n- dynamodb:BatchWriteItem\n- dynamodb:GetItem\n- dynamodb:PutItem\n- dynamodb:Query\n- dynamodb:UpdateItem\n```\n\n### Modeling\nCreate a Pydantic model specifically for storage. This should generally not be shared\nin API contracts or other external interfaces to adhere to single-responsibility principal.\n\n```python\nfrom pydantic import BaseModel\nfrom typing import Optional\n\nclass FilmActor(BaseModel):\n    id: str\n    name: str\n    review: Optional[str]\n    \n```\n\n### Instantiation\nThe repository configuration will dictate the prefix values used for the partition and sort\nkey attributes. Once data is saved, these values cannot be changed without losing\naccess to previously saved data.\n\n`partition_prefix` can be used to categorize data and potentially implement\nrow-based access control. See [DynamoDB docs](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/specifying-conditions.html)\nand [IAM Condition docs](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html).\n**Access control in this manner is only theoretical right now and has not been fully tested.**\n\n`partition_name` is used in conjunction with the `partition_prefix`, with a `#` delimiter,\nto form the partition key value of items within the repository. Individual items can then be\nsaved with their own partition ID value to allow querying based on the sort key values only,\nor they can be saved without their own partition ID to allow querying across the entire \nrepository.\n\n`content_type` is used as the prefix of the sort key value. Typically, this should be set\nto the snake-cased version of the pydantic class name, eg: `movie_character`.\n\nIf all repositories are sharing a single table, it\'s important that each repository \nhas a different combination of the above three values to ensure that the data is segmented.\nYou can choose to point repositories to different tables, but managing capacity becomes\na more complicated problem, which is outside the scope of this library.\n\nThere are two ways to instantiate a repository instance:\n\nThrough the `build` method that will generate the boto3 session and table objects:\n\n```python\nfrom pydantic_dynamo.repository import DynamoRepository\nrepo = DynamoRepository[FilmActor].build(\n    table_name="dynamodb-table-name",\n    item_class=FilmActor,\n    partition_prefix="content",\n    partition_name="movies",\n    content_type="character",\n)\n```\n\nOr directly to the `__init__` if you want control over how the boto3 objects are created:\n\n```python\nfrom pydantic_dynamo.repository import DynamoRepository\nfrom boto3 import Session\n\nresource = Session().resource("dynamodb")\ntable = resource.Table("dynamodb-table-name")\n\nrepo = DynamoRepository[FilmActor](\n    item_class=FilmActor,\n    partition_prefix="content",\n    partition_name="movies",\n    content_type="character",\n    table=table,\n    resource=resource\n)\n```\n### Saving Data\n\nData is saved using an instance of the generic `PartitionedContent[ObjT]` class found in \n[models.py](./pydantic_dynamo/models.py). The `partition_ids` and `content_ids` are `List[str]`. \nEach value in the list is eventually concatenated, and prefixed with the repository\'s configured values.\n\nParticularly for the `content_ids` field, you can leverage this to achieve degrees of query-ability for\nmore complex use cases, eg: `content_ids=["usa", "ny", "saratoga", "12020"]` will result in a \nsort key value of `usa#ny#saratoga#12020` that can be efficiently queried with DynamoDB\'s `begins_with`\ncondition, utilized in this library\'s `list` function.\n\nIt\'s wise to ensure that any values being used in the partition and content IDs are also retained as \nfields on the model object as well, which will make updates easier to perform.\n\n#### Put Single Item\n\nThis is logically similar to the DynamoDB Put operation, and will overwrite an existing item with \nidentical partition and content IDs.\n\n```python\nfrom pydantic_dynamo.models import PartitionedContent\nfrom uuid import uuid4\n\nid1 = str(uuid4())\nactor1 = FilmActor(id=id1, name="Daniel Day-Lewis")\n\nrepo.put(\n    PartitionedContent[FilmActor](\n        partition_ids=[], content_ids=[id1], item=actor1\n    )\n)\n```\n\n#### Put Multiple Items\n\nWhen saving more than one item, you can use a batch operation that will utilize DynamoDB\'s `write_batch` \noperation, which will more efficiently buffer data and minimize the total number of network calls compared to calling\n`put` in a loop.\n\n```python\nfrom pydantic_dynamo.models import PartitionedContent\nfrom uuid import uuid4\n\nid1 = str(uuid4())\nactor1 = FilmActor(id=id1, name="Daniel Day-Lewis")\nid2 = str(uuid4())\nactor2 = FilmActor(id=id2, name="Steve Buscemi")\n\n\nrepo.put_batch(\n    (\n        PartitionedContent[FilmActor](\n            partition_ids=[], content_ids=[id1], item=actor1\n        ),\n        PartitionedContent[FilmActor](\n            partition_ids=[], content_ids=[id2], item=actor2\n        ),\n    )\n)\n```\n\n#### Update an item\n\nNB: Please review the limitation in [issue #1](https://github.com/david-a-jetter/pydantic-dynamo/issues/1)\n\nUpdates are handled in a somewhat more complex and manual manner using an `UpdateCommand` object. \nSince this is constructed by sending `Dict[str, Any]`, dictionary entries are validated against\nthe pydantic model\'s schema before sending data to DynamoDB.\n\n`set_commands` can be used to map attributes\' names to a new value.\n`increment_attrs` can be used to increment attributes\' current values by some integer.\n`append_attrs` can be used to extend a `List` attribute\'s values\n\n`current_version` can be used to enforce a check on the object\'s version number to\nadhere to an object versioning pattern. Since there isn\'t a way the repo currently returns\nand object\'s version, this is not useful at the moment but is an experiment in progress.\n\n\n```python\nfrom pydantic_dynamo.models import UpdateCommand\n\nrepo.update(\n    partition_id=None,\n    content_id=[id1],\n    command=UpdateCommand(\n        set_commands={"review": "Talented, but unfriendly in Gangs of New York"}\n    )\n)\n\n```\n\n### Reading Data\n\n#### Get Item\n\nFinally, something simple to document. This gets a single item by its partition and content IDs,\nreturning `None` if no item is found.\n\nThis example would retrieve just the first actor item.\n```python\nfrom typing import Optional\n\nitem: Optional[FilmActor] = repo.get(partition_id=None, content_id=[id1])\n```\n\n#### Get Multiple Items\n\nThis leverages DynamoDB\'s `batch_get_item` API to collect multiple items by their partition and content IDs.\nThis is often useful after having collected a previous set of records that have potentially related\nitems that you want to retrieve, and then associate the two in a subsequent mapping logic layer.\n\nThis example would retrieve both actor items in a single network request.\n```python\nfrom typing import List\n\nitems: List[FilmActor] = repo.get_batch([(None, [id1]), (None, [id2])])\n\n```\n\n#### Listing Items\nThe following two functions leverage DynamoDB\'s `query` API and offers the ability \nto filter on content ID values, change sort order, limit the quantity of items. \n\nNB: These returns an `Iterator` type, which will not execute any query until it begins iteration.\n\nYou may also pass an optional `FilterCommand` to filter on non-key attributes. All fields\non this object are optional, and are applied utilizing `and` logic.\n\n\n```python\nfrom pydantic_dynamo.models import FilterCommand\n\n# Find actors without a `review` attribute\nfilter1 = FilterCommand(\n    not_exists={"review"}\n)\n\n# Find actors who are talented but unfriendly in Gangs of New York\nfilter2 = FilterCommand(\n    equals={"review": "Talented, but unfriendly in Gangs of New York"}\n)\n\n# Find actors who are not talented but unfriendly in Gangs of New York\nfilter3 = FilterCommand(\n    not_equals={"review": "Talented, but unfriendly in Gangs of New York"}\n)\n\n```\n\n##### List\nThis function supports filter items with a `begins_with` filter on their content IDs.\n\nThis example would retrieve all actor items.\n```python\nfrom typing import Iterator\n\nitems: Iterator[FilmActor] = repo.list(\n    partition_id=None,\n    content_prefix=None,\n    sort_ascending=True, # default order by sort key value\n    limit=None,\n    filters=None\n)\n```\n\n##### List Between\nThis function supports filter items with a `between` filter on their content IDs.\n\nNB: If `content_start == content_end` this will revert to calling `list` using `begins_with`.\n\nThis example would retrieve all actor items. It\'s a lame example and should be updated\nwith something more interesting. A common use case is to include an ISO-formatted datetime\nvalue at the end of a content ID, and you can retrieve all values in a given partition\nbetween two specified datetimes.\n```python\nfrom typing import Iterator\n\nitems: Iterator[FilmActor] = repo.list_between(\n    partition_id=None,\n    content_start=None,\n    content_end=None,\n    sort_ascending=True, # default order by sort key value\n    limit=None,\n    filters=None\n)\n\n```\n',
    'author': 'David Jetter',
    'author_email': 'davidajetter@gmail.com',
    'maintainer': 'None',
    'maintainer_email': 'None',
    'url': 'https://github.com/david-a-jetter/pydantic-dynamo',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.9,<4.0',
}


setup(**setup_kwargs)
