ommi


Nameommi JSON
Version 0.1.8 PyPI version JSON
download
home_pagehttps://github.com/ZechCodes/Ommi
SummaryA portable object model mapper that can work with any database and model library (dataclasses, Attrs, Pydantic, etc.). It is designed for the general case to support the largest possible number of databases.
upload_time2024-06-04 23:22:16
maintainerNone
docs_urlNone
authorZech Zimmerman
requires_python<4.0,>=3.10
licenseMIT
keywords database orm object model mapper dataclasses attrs pydantic sqlite
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Ommi

> [!CAUTION]
> Ommi is under construction and much of the functionality is undergoing frequent revision. There is no guarantee future
> versions will be backwards compatible.

Have you ever needed to use a database for a simple project but didn't want to worry about which database you were going
to use? Or maybe you wanted to create a package that needed to store data but didn't want to force your users to use a
specific database? Meet Ommi, a simple object model mapper that allows you to use whatever models you want to interface
with whatever database you like.

Ommi doesn't provide its own model types, it allows you to use whatever models you are already using. Compatibility with
the most popular model implementations are ensured through a comprehensive unit test suite.

### Compatible Model Implementations

Ommi's test suite checks for compatibility with the following model implementations:

- [Dataclasses](https://docs.python.org/3/library/dataclasses.html)
- [Attrs](https://www.attrs.org/en/stable/)
- [Pydantic](https://docs.pydantic.dev/latest/)

### Included Database Support

#### SQLite3

- Table creation from models
- Select, Insert, Update, Delete, Count
- One-to-one & one-to-many relationships
- Joins to filter queries for a single model type (cannot query for multiple model types in a single query)

#### PostgreSQL

- Table creation from models
- Select, Insert, Update, Delete, Count
- One-to-one & one-to-many relationships
- Joins to filter queries for a single model type (cannot query for multiple model types in a single query)

#### MongoDB

- Collection creation from models
- Fetch, Insert, Update, Delete, Count
- One-to-one & one-to-many relationships
- Joins to filter queries for a single model type (cannot query for multiple model types in a single query)

## Usage

### Defining Models

All models that support Ommi database drivers need to use the `ommi_model` class decorator.

```python
from ommi import ommi_model, Key
from dataclasses import dataclass
from typing import Annotated

@ommi_model
@dataclass
class User:
    name: str
    age: int
    id: Annotated[int, Key] = None  # Optional primary key
```

Models can be assigned to model collections. Any model not assigned a collection will be assigned to a global
collection which can be accessed by calling `ommi.models.get_global_collection()`.

```python
from ommi.models.collections import ModelCollection

collection = ModelCollection()


@ommi_model(collection=collection)
@dataclass
class User:
    name: str
    age: int
```

### Connecting

```python
from ommi.ext.drivers.sqlite import SQLiteDriver, SQLiteConfig


async def example():
    async with SQLiteDriver.from_config(SQLiteConfig(filename=":memory:")) as db:
        ...
```

### Database Actions

The database drivers provide `add`, `count`, `delete`, `fetch`, `sync_schema`, and `update` methods. These methods should
be wrapped in an `ommi.drivers.DatabaseAction`. The database action will capture the return and wrap it in a
`DatabaseStatus` result that is either a `Success` or `Exception`. The database action provides an `or_raise` method
that will force the exception to be raised immediately or return a `Success` result. The `DatabaseStatus` types are
sub-types of `tramp.results.Result` types.

#### Add

Add takes any number of model instances and adds them to the database.

```python
user_1 = User(name="Alice", age=25)
user_2 = User(name="Alice", age=25)
await db.add(user_1, user_2).raise_on_errors()
```

#### Find

Find returns a `FindAction`. This action is used to count, delete, fetch, or set fields on models in the database. It
takes any number of predicates that are AND'd together. It doesn't return any models or make any changes to the database
on its own.

#### Count

Count is an action of `find` that returns the number of models that match the predicates passed to `find`. It returns an
`AsyncResultWrapper` which allows you to access the returned `int` value through chaining, you can read more about it
below.

```python
count = await db.find(User.name == "Alice").count().value
```

#### Delete

Delete is an action of `find` that deletes all models that match the predicates passed to `find`. It also returns
an `AsyncResultWrapper`.

```python
await db.find(User.id == user.id).delete().raise_on_errors()
```

#### Fetch

Fetch is an action of `find` that returns all models that match the predicates passed to `find`. It provides `all` and
`one` helper methods to help with value unpacking, they both raise on errors. Calling `fetch` directly will  return an
`AsyncResultWrapper` that contains the list of models.

```python
users = await db.find(User.name == "Alice").fetch().value
```

```python
users = await db.find(User.name == "Alice").fetch.all()
```

```python
user = await db.find(User.name == "Alice").fetch.one()
```

Models provide a `reload` method that will pull the latest data from the database. It returns an `AsyncResultWrapper`.

```python
await user.reload().raise_on_errors()
```

#### Set

Set is an action of `find` that updates all models that match the predicates passed to `find`. It takes a any number of
keyword arguments that are used to update the models fields. It returns an `AsyncResultWrapper`.

```python
await db.find(name="bob").set(name = "Bob").raise_on_errors()
```

Models provide a `save` method that will push changed fields to the database. It returns an
`AsyncResultWrapper`.

```python
user.name = "Bob"
await user.save().raise_on_errors()
```

#### Schema

Schema is an action object that provides methods to manipulate the database itself. It can optionally be passed an
optional `ModelCollection` or nothing to default to the global collection.

Its `create_models` action returns an `AsyncResultWrapper` that contains a list of the models that were created.

```python
await db.schema().create_models().raise_on_errors()
```

```python
await db.schema(some_model_collection).create_models().raise_on_errors()
```

It also provides a `delete_models` action that deletes all models in the collection from the database.

```python
await db.schema(some_model_collection).delete_models().raise_on_errors()
```

### AsyncResultWrapper

`AsyncResultWrapper` is a wrapper around the result of an async database action. It provides various awaitable
properties and methods that allow you to access the result of the action. Awaiting the `AsyncResultWrapper` itself will
return a `DatabaseResult.Success` object if the action succeeded or a `DatabaseResult.Failure` object if there was an
exception.

```python
match await db.find(User.name == "Alice").count():
    case DatabaseResult.Success(value):
        print(value)

    case DatabaseResult.Failure(error):
        print(error)
```

#### Value

Value is an awaitable property that returns the value of the action. It will raise an exception if the action failed.

```python
await db.find(User.name == "Alice").count().value
```

#### Value Or

Value or is a method that takes a default value and returns the value of the action or the default value if the action
failed.

```python
count = await db.find(User.name == "Alice").count().value_or(0)
```

#### Raise on Errors

Raise on errors is a method that will raise an exception if the action failed. If the action succeeds it'll return
nothing. It's a convenience method that allows you to raise errors and discard the result on success.

```python
await db.find(User.name == "Alice").delete().raise_on_errors()
```

### Database Results

`DatabaseResult` is a result type that is used to wrap the values of database actions. It provides a `Success` and
`Failure` type that can be used to match on the result of an action.

```python
match await db.find(User.name == "Alice").count():
    case DatabaseResult.Success(value):
        print(value)

    case DatabaseResult.Failure(error):
        print(error)
```

#### Value and Value Or

`DatabaseResult` objects provide a `value` property that returns the value of the action or raises an exception if the
action failed. It also provides a `value_or` that takes a default value that is returned if the action failed.

Unlike `AsyncResultWrapper`, `DatabaseResult` objects do not need to be awaited.

```python
result = await db.find(User.name == "Alice").count()
print(result.value)  # Raises an exception if the action failed
```

```python
result = await db.find(User.name == "Alice").count()
print(result.value_or(0))  # Prints 0 if the action failed
```

#### Error

`DatabaseResult` objects provide an `error` property that returns the exception that caused the action to fail or `None`
if the action succeeded.

```python
result = await db.find(User.name == "Alice").count()
print(result.error)  # Prints None if the action succeeded
```

### Lazy Loaded Relationships

Ommi provides support for lazy loading relationships between models. It fully supports forward references as string
annotations. There are two supported relationship types using the `ommi.query_fields.LazyLoadTheRelated` and
`ommi.query_fields.LazyLoadEveryRelated` generic types as annotations. It relies on models using the
`ommi.field_metadata.ReferenceTo` annotation to define which field references which field on another model.

```python
@ommi_model
@dataclass
class User:
    id: int

    posts: LazyLoadEveryRelated["Post"]

class Post:
    id: int
    author_id: Annotated[int, ReferenceTo(User)]

    author: LazyLoadTheRelated[User]
```

`LazyLoadTheRelated` and `LazyLoadEveryRelated` are awaitable to fetch the related models. They also provide an
awaitable `value` property that returns the same value as well as a `get` method that takes a default value in case of a
failure. `LazyLoadTheRelated` will return a single model while `LazyLoadEveryRelated` will return a `list` of models.

```python
user = User(id=1)
posts = await user.posts
```

Lazy fields will only fetch once and then cache the result.

#### Get

'LazyQueryFields` provide a `get` method that takes a default value and returns the value of the relationship or the
default value if there is an error.

```python
user = User(id=1)
posts = await user.posts.get([])
```

### Value

`LazyQueryFields` provide a `value` property that returns the value of the relationship or raises an exception if there
is an error.

```python
user = User(id=1)
posts = await user.posts.value
```

### Refresh

`LazyQueryFields` provide a `refresh` method that will fetch the related models again and update the cache.

```python
user = User(id=1)
await user.posts.refresh()
```

### Refresh if needed

The `refresh_if_needed` method will only fetch the related models if they haven't been fetched yet.

```python
user = User(id=1)
await user.posts.refresh_if_needed()
```

### Result

`LazyQueryFields` provide a `result` property that returns the `tramp.results.Result` object directly. This can be
helpful for handling errors more explicitly.

```python
user = User(id=1)
match await user.posts.result:
    case Value(posts):
        ...

    case Error(error):
        ...
```
            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/ZechCodes/Ommi",
    "name": "ommi",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.10",
    "maintainer_email": null,
    "keywords": "database, orm, object model mapper, dataclasses, attrs, pydantic, sqlite",
    "author": "Zech Zimmerman",
    "author_email": "hi@zech.codes",
    "download_url": "https://files.pythonhosted.org/packages/fd/9a/8df6d1b4a64e8a9d48064866ca0effd3cc0dc27b6ab5c87a24a59ee0ce32/ommi-0.1.8.tar.gz",
    "platform": null,
    "description": "# Ommi\n\n> [!CAUTION]\n> Ommi is under construction and much of the functionality is undergoing frequent revision. There is no guarantee future\n> versions will be backwards compatible.\n\nHave you ever needed to use a database for a simple project but didn't want to worry about which database you were going\nto use? Or maybe you wanted to create a package that needed to store data but didn't want to force your users to use a\nspecific database? Meet Ommi, a simple object model mapper that allows you to use whatever models you want to interface\nwith whatever database you like.\n\nOmmi doesn't provide its own model types, it allows you to use whatever models you are already using. Compatibility with\nthe most popular model implementations are ensured through a comprehensive unit test suite.\n\n### Compatible Model Implementations\n\nOmmi's test suite checks for compatibility with the following model implementations:\n\n- [Dataclasses](https://docs.python.org/3/library/dataclasses.html)\n- [Attrs](https://www.attrs.org/en/stable/)\n- [Pydantic](https://docs.pydantic.dev/latest/)\n\n### Included Database Support\n\n#### SQLite3\n\n- Table creation from models\n- Select, Insert, Update, Delete, Count\n- One-to-one & one-to-many relationships\n- Joins to filter queries for a single model type (cannot query for multiple model types in a single query)\n\n#### PostgreSQL\n\n- Table creation from models\n- Select, Insert, Update, Delete, Count\n- One-to-one & one-to-many relationships\n- Joins to filter queries for a single model type (cannot query for multiple model types in a single query)\n\n#### MongoDB\n\n- Collection creation from models\n- Fetch, Insert, Update, Delete, Count\n- One-to-one & one-to-many relationships\n- Joins to filter queries for a single model type (cannot query for multiple model types in a single query)\n\n## Usage\n\n### Defining Models\n\nAll models that support Ommi database drivers need to use the `ommi_model` class decorator.\n\n```python\nfrom ommi import ommi_model, Key\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\n@ommi_model\n@dataclass\nclass User:\n    name: str\n    age: int\n    id: Annotated[int, Key] = None  # Optional primary key\n```\n\nModels can be assigned to model collections. Any model not assigned a collection will be assigned to a global\ncollection which can be accessed by calling `ommi.models.get_global_collection()`.\n\n```python\nfrom ommi.models.collections import ModelCollection\n\ncollection = ModelCollection()\n\n\n@ommi_model(collection=collection)\n@dataclass\nclass User:\n    name: str\n    age: int\n```\n\n### Connecting\n\n```python\nfrom ommi.ext.drivers.sqlite import SQLiteDriver, SQLiteConfig\n\n\nasync def example():\n    async with SQLiteDriver.from_config(SQLiteConfig(filename=\":memory:\")) as db:\n        ...\n```\n\n### Database Actions\n\nThe database drivers provide `add`, `count`, `delete`, `fetch`, `sync_schema`, and `update` methods. These methods should\nbe wrapped in an `ommi.drivers.DatabaseAction`. The database action will capture the return and wrap it in a\n`DatabaseStatus` result that is either a `Success` or `Exception`. The database action provides an `or_raise` method\nthat will force the exception to be raised immediately or return a `Success` result. The `DatabaseStatus` types are\nsub-types of `tramp.results.Result` types.\n\n#### Add\n\nAdd takes any number of model instances and adds them to the database.\n\n```python\nuser_1 = User(name=\"Alice\", age=25)\nuser_2 = User(name=\"Alice\", age=25)\nawait db.add(user_1, user_2).raise_on_errors()\n```\n\n#### Find\n\nFind returns a `FindAction`. This action is used to count, delete, fetch, or set fields on models in the database. It\ntakes any number of predicates that are AND'd together. It doesn't return any models or make any changes to the database\non its own.\n\n#### Count\n\nCount is an action of `find` that returns the number of models that match the predicates passed to `find`. It returns an\n`AsyncResultWrapper` which allows you to access the returned `int` value through chaining, you can read more about it\nbelow.\n\n```python\ncount = await db.find(User.name == \"Alice\").count().value\n```\n\n#### Delete\n\nDelete is an action of `find` that deletes all models that match the predicates passed to `find`. It also returns\nan `AsyncResultWrapper`.\n\n```python\nawait db.find(User.id == user.id).delete().raise_on_errors()\n```\n\n#### Fetch\n\nFetch is an action of `find` that returns all models that match the predicates passed to `find`. It provides `all` and\n`one` helper methods to help with value unpacking, they both raise on errors. Calling `fetch` directly will  return an\n`AsyncResultWrapper` that contains the list of models.\n\n```python\nusers = await db.find(User.name == \"Alice\").fetch().value\n```\n\n```python\nusers = await db.find(User.name == \"Alice\").fetch.all()\n```\n\n```python\nuser = await db.find(User.name == \"Alice\").fetch.one()\n```\n\nModels provide a `reload` method that will pull the latest data from the database. It returns an `AsyncResultWrapper`.\n\n```python\nawait user.reload().raise_on_errors()\n```\n\n#### Set\n\nSet is an action of `find` that updates all models that match the predicates passed to `find`. It takes a any number of\nkeyword arguments that are used to update the models fields. It returns an `AsyncResultWrapper`.\n\n```python\nawait db.find(name=\"bob\").set(name = \"Bob\").raise_on_errors()\n```\n\nModels provide a `save` method that will push changed fields to the database. It returns an\n`AsyncResultWrapper`.\n\n```python\nuser.name = \"Bob\"\nawait user.save().raise_on_errors()\n```\n\n#### Schema\n\nSchema is an action object that provides methods to manipulate the database itself. It can optionally be passed an\noptional `ModelCollection` or nothing to default to the global collection.\n\nIts `create_models` action returns an `AsyncResultWrapper` that contains a list of the models that were created.\n\n```python\nawait db.schema().create_models().raise_on_errors()\n```\n\n```python\nawait db.schema(some_model_collection).create_models().raise_on_errors()\n```\n\nIt also provides a `delete_models` action that deletes all models in the collection from the database.\n\n```python\nawait db.schema(some_model_collection).delete_models().raise_on_errors()\n```\n\n### AsyncResultWrapper\n\n`AsyncResultWrapper` is a wrapper around the result of an async database action. It provides various awaitable\nproperties and methods that allow you to access the result of the action. Awaiting the `AsyncResultWrapper` itself will\nreturn a `DatabaseResult.Success` object if the action succeeded or a `DatabaseResult.Failure` object if there was an\nexception.\n\n```python\nmatch await db.find(User.name == \"Alice\").count():\n    case DatabaseResult.Success(value):\n        print(value)\n\n    case DatabaseResult.Failure(error):\n        print(error)\n```\n\n#### Value\n\nValue is an awaitable property that returns the value of the action. It will raise an exception if the action failed.\n\n```python\nawait db.find(User.name == \"Alice\").count().value\n```\n\n#### Value Or\n\nValue or is a method that takes a default value and returns the value of the action or the default value if the action\nfailed.\n\n```python\ncount = await db.find(User.name == \"Alice\").count().value_or(0)\n```\n\n#### Raise on Errors\n\nRaise on errors is a method that will raise an exception if the action failed. If the action succeeds it'll return\nnothing. It's a convenience method that allows you to raise errors and discard the result on success.\n\n```python\nawait db.find(User.name == \"Alice\").delete().raise_on_errors()\n```\n\n### Database Results\n\n`DatabaseResult` is a result type that is used to wrap the values of database actions. It provides a `Success` and\n`Failure` type that can be used to match on the result of an action.\n\n```python\nmatch await db.find(User.name == \"Alice\").count():\n    case DatabaseResult.Success(value):\n        print(value)\n\n    case DatabaseResult.Failure(error):\n        print(error)\n```\n\n#### Value and Value Or\n\n`DatabaseResult` objects provide a `value` property that returns the value of the action or raises an exception if the\naction failed. It also provides a `value_or` that takes a default value that is returned if the action failed.\n\nUnlike `AsyncResultWrapper`, `DatabaseResult` objects do not need to be awaited.\n\n```python\nresult = await db.find(User.name == \"Alice\").count()\nprint(result.value)  # Raises an exception if the action failed\n```\n\n```python\nresult = await db.find(User.name == \"Alice\").count()\nprint(result.value_or(0))  # Prints 0 if the action failed\n```\n\n#### Error\n\n`DatabaseResult` objects provide an `error` property that returns the exception that caused the action to fail or `None`\nif the action succeeded.\n\n```python\nresult = await db.find(User.name == \"Alice\").count()\nprint(result.error)  # Prints None if the action succeeded\n```\n\n### Lazy Loaded Relationships\n\nOmmi provides support for lazy loading relationships between models. It fully supports forward references as string\nannotations. There are two supported relationship types using the `ommi.query_fields.LazyLoadTheRelated` and\n`ommi.query_fields.LazyLoadEveryRelated` generic types as annotations. It relies on models using the\n`ommi.field_metadata.ReferenceTo` annotation to define which field references which field on another model.\n\n```python\n@ommi_model\n@dataclass\nclass User:\n    id: int\n\n    posts: LazyLoadEveryRelated[\"Post\"]\n\nclass Post:\n    id: int\n    author_id: Annotated[int, ReferenceTo(User)]\n\n    author: LazyLoadTheRelated[User]\n```\n\n`LazyLoadTheRelated` and `LazyLoadEveryRelated` are awaitable to fetch the related models. They also provide an\nawaitable `value` property that returns the same value as well as a `get` method that takes a default value in case of a\nfailure. `LazyLoadTheRelated` will return a single model while `LazyLoadEveryRelated` will return a `list` of models.\n\n```python\nuser = User(id=1)\nposts = await user.posts\n```\n\nLazy fields will only fetch once and then cache the result.\n\n#### Get\n\n'LazyQueryFields` provide a `get` method that takes a default value and returns the value of the relationship or the\ndefault value if there is an error.\n\n```python\nuser = User(id=1)\nposts = await user.posts.get([])\n```\n\n### Value\n\n`LazyQueryFields` provide a `value` property that returns the value of the relationship or raises an exception if there\nis an error.\n\n```python\nuser = User(id=1)\nposts = await user.posts.value\n```\n\n### Refresh\n\n`LazyQueryFields` provide a `refresh` method that will fetch the related models again and update the cache.\n\n```python\nuser = User(id=1)\nawait user.posts.refresh()\n```\n\n### Refresh if needed\n\nThe `refresh_if_needed` method will only fetch the related models if they haven't been fetched yet.\n\n```python\nuser = User(id=1)\nawait user.posts.refresh_if_needed()\n```\n\n### Result\n\n`LazyQueryFields` provide a `result` property that returns the `tramp.results.Result` object directly. This can be\nhelpful for handling errors more explicitly.\n\n```python\nuser = User(id=1)\nmatch await user.posts.result:\n    case Value(posts):\n        ...\n\n    case Error(error):\n        ...\n```",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "A portable object model mapper that can work with any database and model library (dataclasses, Attrs, Pydantic, etc.). It is designed for the general case to support the largest possible number of databases.",
    "version": "0.1.8",
    "project_urls": {
        "Homepage": "https://github.com/ZechCodes/Ommi"
    },
    "split_keywords": [
        "database",
        " orm",
        " object model mapper",
        " dataclasses",
        " attrs",
        " pydantic",
        " sqlite"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "a2ff3e989b47afc37a8b258c0f340ebd11947c622a4f66abc376cf77a53a3439",
                "md5": "4e4d4c1c77652e0adf622dbb4c87a1bc",
                "sha256": "8e5cf67c9b4c8d379c40a515e0f60d72bf049efe4167a1540a48eec8a23af8ef"
            },
            "downloads": -1,
            "filename": "ommi-0.1.8-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "4e4d4c1c77652e0adf622dbb4c87a1bc",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.10",
            "size": 52147,
            "upload_time": "2024-06-04T23:22:13",
            "upload_time_iso_8601": "2024-06-04T23:22:13.202009Z",
            "url": "https://files.pythonhosted.org/packages/a2/ff/3e989b47afc37a8b258c0f340ebd11947c622a4f66abc376cf77a53a3439/ommi-0.1.8-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "fd9a8df6d1b4a64e8a9d48064866ca0effd3cc0dc27b6ab5c87a24a59ee0ce32",
                "md5": "b7949991c430777ccfad6c48f500c915",
                "sha256": "35055b9dcc67b04a1d13d04e476de8823e55108ef1a790a4c66e887df5eba1e2"
            },
            "downloads": -1,
            "filename": "ommi-0.1.8.tar.gz",
            "has_sig": false,
            "md5_digest": "b7949991c430777ccfad6c48f500c915",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.10",
            "size": 32668,
            "upload_time": "2024-06-04T23:22:16",
            "upload_time_iso_8601": "2024-06-04T23:22:16.319304Z",
            "url": "https://files.pythonhosted.org/packages/fd/9a/8df6d1b4a64e8a9d48064866ca0effd3cc0dc27b6ab5c87a24a59ee0ce32/ommi-0.1.8.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-06-04 23:22:16",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "ZechCodes",
    "github_project": "Ommi",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "ommi"
}
        
Elapsed time: 0.25730s