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

packages = \
['fastapi_motor_oil']

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

install_requires = \
['motor>=3.1.1,<4.0.0', 'pydantic>=1.10.5,<2.0.0']

setup_kwargs = {
    'name': 'fastapi-motor-oil',
    'version': '0.3.0',
    'description': 'Typed utilities for using MongoDB (and the asyncio motor driver) with FastAPI - not an ODM.',
    'long_description': '# fastapi-motor-oil\n\nTyped utilities for using MongoDB (and the async `motor` driver) with FastAPI - not an ODM.\n\n## Installation\n\nYou can install the library and its dependencies with `pip install fastapi-motor-oil`.\n\n## Example\n\nPrerequisites:\n\n- MongoDB (e.g. the Community Edition) installed and running locally;\n- `fastapi` with all its dependencies (`pip install fastapi[all]`);\n- This library (`pip install fastapi-motor-oil`).\n\nIn this example we will create:\n\n- a simple `TreeNode` document model with a `name` and an optional reference to a `parent` node and some delete rules;\n- the services that are necessary to create, read, update, and delete documents;\n- a `fastapi` `APIRouter` factory that can be included in `fastapi` applications;\n- and the `fastapi` application itself.\n\nThe project layout under your root directory will be as follows:\n\n- `/tree_app`\n  - `__init__.py`\n  - `api.py`\n  - `app.py`\n  - `model.py`\n  - `service.py`\n\nModel definitions (in `model.py`):\n\n```python\nfrom fastapi_motor_oil import DocumentModel, StrObjectId\nfrom pydantic import BaseModel\n\nclass TreeNode(DocumentModel):\n    """\n    Tree node document model.\n    """\n\n    name: str\n    parent: StrObjectId | None\n\nclass TreeNodeCreate(BaseModel):\n    """\n    Tree node creation model.\n    """\n\n    name: str\n    parent: StrObjectId | None\n\nclass TreeNodeUpdate(BaseModel):\n    """\n    Tree node update model.\n    """\n\n    name: str | None\n    parent: StrObjectId | None\n```\n\nService implementation (in `service.py`):\n\n```python\nfrom collections.abc import Sequence\n\nfrom bson import ObjectId\nfrom fastapi_motor_oil import CollectionOptions, MongoService, delete_rule, validator\nfrom motor.core import AgnosticClientSession\n\nfrom .model import TreeNodeCreate, TreeNodeUpdate\n\nclass TreeNodeService(MongoService[TreeNodeCreate, TreeNodeUpdate]):\n    """\n    Tree node database services.\n    """\n\n    __slots__ = ()\n\n    collection_name: str = "tree_nodes"\n\n    collection_options: CollectionOptions | None = None\n\n    @delete_rule("pre")  # Delete rule that remove the subtrees of deleted nodes.\n    async def dr_delete_subtree(self, session: AgnosticClientSession, ids: Sequence[ObjectId]) -> None:\n        child_ids = await self.find_ids({"parent": {"$in": ids}}, session=session)\n        if len(child_ids) > 0:\n            # Recursion\n            await self.delete_many({"_id": {"$in": child_ids}}, options={"session": session})\n\n    @delete_rule("deny")  # Delete rule that prevents the removal of root nodes.\n    async def dr_deny_if_root(self, session: AgnosticClientSession, ids: Sequence[ObjectId]) -> None:\n        root_cnt = await self.count_documents(\n            {"$and": [{"_id": {"$in": ids}}, {"parent": None}]},\n            options={"session": session},\n        )\n        if root_cnt > 0:\n            raise ValueError("Can not delete root nodes.")\n\n    @validator("insert-update")\n    async def v_parent_valid(self, query: MongoQuery | None, data: TreeNodeCreate | TreeNodeUpdate) -> None:\n        if data.parent is None:  # No parent node is always fine\n            return\n\n        if not await self.exists(data.parent):  # Parent must exist.\n            raise ValueError("Parent does not exist.")\n\n        if isinstance(data, TreeNodeCreate):  # No more check during creation.\n            return\n\n        matched_ids = (await self.find_ids(query)) if isinstance(data, TreeNodeUpdate) else []\n        if data.parent in matched_ids:  # Self reference is forbidden.\n            raise ValueError("Self-reference.")\n```\n\nRouting implementation (in `api.py`):\n\n```python\nfrom typing import Any\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom fastapi_motor_oil import AgnosticDatabase, DatabaseProvider, DeleteResultModel, StrObjectId\n\nfrom .model import TreeNode, TreeNodeCreate, TreeNodeUpdate\nfrom .service import TreeNodeService\n\ndef make_api(\n    *,\n    get_database: DatabaseProvider,\n    prefix: str = "/tree-node",\n) -> APIRouter:\n    """\n    Tree node `APIRouter` factory.\n\n    Arguments:\n        get_database: FastAPI dependency that returns the `AgnosticDatabase`\n                      database instance for the API.\n        prefix: The prefix for the created `APIRouter`.\n        add_get_all: Whether to add the "get all" route to the router.\n        add_create: Whether to add the "create" route to the router.\n        add_get_by_id: Whether to add the "get by ID" route to the router.\n        add_delete: Whether to add the "delete" route to the router.\n        add_update: Whether to add the "update" route to the router.\n\n    Returns:\n        The created `APIRouter` instance.\n    """\n    api = APIRouter(prefix=prefix)\n\n    @api.get("/", response_model=list[TreeNode])\n    async def get_all(\n        database: AgnosticDatabase = Depends(get_database),\n    ) -> list[dict[str, Any]]:\n        svc = TreeNodeService(database)\n        return [d async for d in svc.find()]\n\n    @api.post("/", response_model=TreeNode)\n    async def create(\n        data: TreeNodeCreate,\n        database: AgnosticDatabase = Depends(get_database),\n    ) -> dict[str, Any]:\n        svc = TreeNodeService(database)\n\n        try:\n            result = await svc.insert_one(data)\n        except Exception:\n            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Creation failed.")\n\n        if (created := await svc.get_by_id(result.inserted_id)) is not None:\n            return created\n\n        raise HTTPException(status.HTTP_409_CONFLICT)\n\n    @api.get("/{id}", response_model=TreeNode)\n    async def get_by_id(\n        id: StrObjectId,\n        database: AgnosticDatabase = Depends(get_database),\n    ) -> dict[str, Any]:\n        svc = TreeNodeService(database)\n        if (result := await svc.get_by_id(id)) is not None:\n            return result\n\n        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(id))\n\n    @api.put("/{id}", response_model=TreeNode)\n    async def update_by_id(\n        id: StrObjectId,\n        data: TreeNodeUpdate,\n        database: AgnosticDatabase = Depends(get_database),\n    ) -> dict[str, Any]:\n        svc = TreeNodeService(database)\n\n        try:\n            result = await svc.update_by_id(id, data)\n        except Exception:\n            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(id))\n\n        if result.matched_count == 0:\n            raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(id))\n\n        if (updated := await svc.get_by_id(id)) is not None:\n            return updated\n\n        raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(id))\n\n    @api.delete("/{id}", response_model=DeleteResultModel)\n    async def delete_by_id(\n        id: StrObjectId,\n        database: AgnosticDatabase = Depends(get_database),\n    ) -> DeleteResultModel:\n        svc = TreeNodeService(database)\n        result = await svc.delete_by_id(id)\n        if result.deleted_count == 0:\n            raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(id))\n\n        return DeleteResultModel(delete_count=result.deleted_count)\n\n    return api\n```\n\nApplication (in `app.py`):\n\n```python\nfrom functools import lru_cache\n\nfrom fastapi import FastAPI\nfrom motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase\n\n@lru_cache(maxsize=1)\ndef get_database() -> AsyncIOMotorDatabase:\n    """Database provider dependency for the created API."""\n    mongo_connection_string = "mongodb://127.0.0.1:27017"\n    database_name = "tree-db"\n    client = AsyncIOMotorClient(mongo_connection_string)\n    return client[database_name]\n\ndef register_routes(app: FastAPI) -> None:\n    """Registers all routes of the application."""\n    from .api import make_tree_node_api\n\n    api_prefix = "/api/v1"\n\n    app.include_router(\n        make_tree_node_api(get_database=get_database),\n        prefix=api_prefix,\n    )\n\ndef create_app() -> FastAPI:\n    app = FastAPI()\n\n    register_routes(app)\n\n    return app\n```\n\nAdd `__init__.py` as well to `tree_app`:\n\n```python\nfrom .app import create_app\n```\n\nWith everything in place, you can serve the application by executing `uvicorn tree_app:create_app --reload --factory` in your root directory. Go to [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) in the browser to see and try the created REST API.\n\n## Requirements\n\nThe project depends on `motor` (the official asyncio MongoDB driver, which is built on top of `pymongo` and `bson`) and `pydantic`.\n\n`fastapi` is not an actual dependency, but the code was written with `fastapi` applications with a REST API in mind.\n\n## Development\n\nUse `black` for code formatting and `mypy` for static code analysis.\n\n## Contributing\n\nContributions are welcome.\n\n## License - MIT\n\nThe library is open-sourced under the conditions of the MIT [license](https://choosealicense.com/licenses/mit/).\n',
    'author': 'Peter Volf',
    'author_email': 'do.volfp@gmail.com',
    'maintainer': 'None',
    'maintainer_email': 'None',
    'url': 'None',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.10,<4.0',
}


setup(**setup_kwargs)
