diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | python-ormar.spec | 2177 | ||||
| -rw-r--r-- | sources | 1 | 
3 files changed, 2179 insertions, 0 deletions
| @@ -0,0 +1 @@ +/ormar-0.12.1.tar.gz diff --git a/python-ormar.spec b/python-ormar.spec new file mode 100644 index 0000000..7ad7ef0 --- /dev/null +++ b/python-ormar.spec @@ -0,0 +1,2177 @@ +%global _empty_manifest_terminate_build 0 +Name:		python-ormar +Version:	0.12.1 +Release:	1 +Summary:	A simple async ORM with fastapi in mind and pydantic validation. +License:	MIT +URL:		https://github.com/collerek/ormar +Source0:	https://mirrors.nju.edu.cn/pypi/web/packages/1e/86/2ab5ae0e83184d07f53691f27af998a29b125d9928bfc6ce7ab76169b292/ormar-0.12.1.tar.gz +BuildArch:	noarch + +Requires:	python3-databases +Requires:	python3-pydantic +Requires:	python3-SQLAlchemy +Requires:	python3-cryptography +Requires:	python3-aiosqlite +Requires:	python3-aiomysql +Requires:	python3-aiopg +Requires:	python3-asyncpg +Requires:	python3-psycopg2-binary +Requires:	python3-mysqlclient +Requires:	python3-PyMySQL +Requires:	python3-orjson +Requires:	python3-typing-extensions +Requires:	python3-importlib-metadata + +%description +# ormar +<p> +<a href="https://pypi.org/project/ormar"> +    <img src="https://img.shields.io/pypi/v/ormar.svg" alt="Pypi version"> +</a> +<a href="https://pypi.org/project/ormar"> +    <img src="https://img.shields.io/pypi/pyversions/ormar.svg" alt="Pypi version"> +</a> +<img src="https://github.com/collerek/ormar/workflows/build/badge.svg" alt="Build Status"> +<a href="https://codecov.io/gh/collerek/ormar"> +    <img src="https://codecov.io/gh/collerek/ormar/branch/master/graph/badge.svg" alt="Coverage"> +</a> +<a href="https://www.codefactor.io/repository/github/collerek/ormar"> +<img src="https://www.codefactor.io/repository/github/collerek/ormar/badge" alt="CodeFactor" /> +</a> +<a href="https://codeclimate.com/github/collerek/ormar/maintainability"> +<img src="https://api.codeclimate.com/v1/badges/186bc79245724864a7aa/maintainability" /></a> +<a href="https://pepy.tech/project/ormar"> +<img src="https://pepy.tech/badge/ormar"></a> +</p> + +### Overview + +The `ormar` package is an async mini ORM for Python, with support for **Postgres, +MySQL**, and **SQLite**. + +The main benefits of using `ormar` are: + +*  getting an **async ORM that can be used with async frameworks** (fastapi, starlette etc.) +*  getting just **one model to maintain** - you don't have to maintain pydantic and other orm models (sqlalchemy, peewee, gino etc.) + +The goal was to create a simple ORM that can be **used directly (as request and response models) with [`fastapi`][fastapi]** that bases it's data validation on pydantic. + +Ormar - apart from the obvious "ORM" in name - gets its name from _ormar_ in Swedish which means _snakes_, and _ormar_ in Croatian which means _cabinet_. + +And what's a better name for python ORM than snakes cabinet :) + +**If you like ormar remember to star the repository in [github](https://github.com/collerek/ormar)!** + +The bigger community we build, the easier it will be to catch bugs and attract contributors ;) + +### Documentation + +Check out the [documentation][documentation] for details. + +**Note that for brevity most of the documentation snippets omit the creation of the database +and scheduling the execution of functions for asynchronous run.** + +If you want more real life examples than in the documentation you can see the [tests][tests] folder, +since they actually have to create and connect to a database in most of the tests. + +Yet remember that those are - well - tests and not all solutions are suitable to be used in real life applications. + +### Part of the `fastapi` ecosystem + +As part of the fastapi ecosystem `ormar` is supported in libraries that somehow work with databases. + +As of now `ormar` is supported by: + +*  [`fastapi-users`](https://github.com/frankie567/fastapi-users) +*  [`fastapi-crudrouter`](https://github.com/awtkns/fastapi-crudrouter) +*  [`fastapi-pagination`](https://github.com/uriyyo/fastapi-pagination) + +If you maintain or use a different library and would like it to support `ormar` let us know how we can help. + +### Dependencies + +Ormar is built with: + +* [`sqlalchemy core`][sqlalchemy-core] for query building. +* [`databases`][databases] for cross-database async support. +* [`pydantic`][pydantic] for data validation. +* `typing_extensions` for python 3.6 - 3.7 + +### License + +`ormar` is built as open-sorce software and will remain completely free (MIT license). + +As I write open-source code to solve everyday problems in my work or to promote and build strong python +community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained. + +<a aria-label="Sponsor collerek" href="https://github.com/sponsors/collerek" style="text-decoration: none; color: #c9d1d9 !important;"> +<div style=" +    background-color: #21262d; +    border-color: #30363d; +    box-shadow:  0 0 transparent, 0 0 transparent; +    color: #c9d1d9 !important; +    border: 1px solid; +    border-radius: 6px; +    cursor: pointer; +    display: inline-block; +    font-size: 14px; +    padding: 10px; +    line-height: 0px; +    height: 40px; +"> +<span style="color: #c9d1d9 !important;">Sponsor - Github Sponsors</span> +</div> +</a> + +### Migrating from `sqlalchemy` and existing databases + +If you currently use `sqlalchemy` and would like to switch to `ormar` check out the auto-translation +tool that can help you with translating existing sqlalchemy orm models so you do not have to do it manually. + +**Beta** versions available at github: [`sqlalchemy-to-ormar`](https://github.com/collerek/sqlalchemy-to-ormar) +or simply `pip install sqlalchemy-to-ormar` + +`sqlalchemy-to-ormar` can be used in pair with `sqlacodegen` to auto-map/ generate `ormar` models from existing database, even if you don't use `sqlalchemy` for your project. + +### Migrations & Database creation + +Because ormar is built on SQLAlchemy core, you can use [`alembic`][alembic] to provide +database migrations (and you really should for production code). + +For tests and basic applications the `sqlalchemy` is more than enough: +```python +# note this is just a partial snippet full working example below +# 1. Imports +import sqlalchemy +import databases + +# 2. Initialization +DATABASE_URL = "sqlite:///db.sqlite" +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() + +# Define models here + +# 3. Database creation and tables creation +engine = sqlalchemy.create_engine(DATABASE_URL) +metadata.create_all(engine) +``` + +For a sample configuration of alembic and more information regarding migrations and +database creation visit [migrations][migrations] documentation section. + +### Package versions +**ormar is still under development:** +We recommend pinning any dependencies (with i.e. `ormar~=0.9.1`) + +`ormar` also follows the release numeration that breaking changes bump the major number, +while other changes and fixes bump minor number, so with the latter you should be safe to +update, yet always read the [releases][releases] docs before. +`example: (0.5.2 -> 0.6.0 - breaking, 0.5.2 -> 0.5.3 - non breaking)`. + +### Asynchronous Python + +Note that `ormar` is an asynchronous ORM, which means that you have to `await` the calls to +the methods, that are scheduled for execution in an event loop. Python has a builtin module +[`asyncio`][asyncio] that allows you to do just that. + +Note that most "normal" python interpreters do not allow execution of `await` +outside of a function (because you actually schedule this function for delayed execution +and don't get the result immediately). + +In a modern web framework (like `fastapi`), the framework will handle this for you, but if +you plan to do this on your own you need to perform this manually like described in the +quick start below. + +### Quick Start + +Note that you can find the same script in examples folder on github. + +```python +from typing import Optional + +import databases +import pydantic + +import ormar +import sqlalchemy + +DATABASE_URL = "sqlite:///db.sqlite" +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() + + +# note that this step is optional -> all ormar cares is a internal +# class with name Meta and proper parameters, but this way you do not +# have to repeat the same parameters if you use only one database +class BaseMeta(ormar.ModelMeta): +    metadata = metadata +    database = database + + +# Note that all type hints are optional +# below is a perfectly valid model declaration +# class Author(ormar.Model): +#     class Meta(BaseMeta): +#         tablename = "authors" +# +#     id = ormar.Integer(primary_key=True) # <= notice no field types +#     name = ormar.String(max_length=100) + + +class Author(ormar.Model): +    class Meta(BaseMeta): +        tablename = "authors" + +    id: int = ormar.Integer(primary_key=True) +    name: str = ormar.String(max_length=100) + + +class Book(ormar.Model): +    class Meta(BaseMeta): +        tablename = "books" + +    id: int = ormar.Integer(primary_key=True) +    author: Optional[Author] = ormar.ForeignKey(Author) +    title: str = ormar.String(max_length=100) +    year: int = ormar.Integer(nullable=True) + + +# create the database +# note that in production you should use migrations +# note that this is not required if you connect to existing database +engine = sqlalchemy.create_engine(DATABASE_URL) +# just to be sure we clear the db before +metadata.drop_all(engine) +metadata.create_all(engine) + + +# all functions below are divided into functionality categories +# note how all functions are defined with async - hence can use await AND needs to +# be awaited on their own +async def create(): +    # Create some records to work with through QuerySet.create method. +    # Note that queryset is exposed on each Model's class as objects +    tolkien = await Author.objects.create(name="J.R.R. Tolkien") +    await Book.objects.create(author=tolkien, title="The Hobbit", year=1937) +    await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955) +    await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977) + +    # alternative creation of object divided into 2 steps +    sapkowski = Author(name="Andrzej Sapkowski") +    # do some stuff +    await sapkowski.save() + +    # or save() after initialization +    await Book(author=sapkowski, title="The Witcher", year=1990).save() +    await Book(author=sapkowski, title="The Tower of Fools", year=2002).save() + +    # to read more about inserting data into the database +    # visit: https://collerek.github.io/ormar/queries/create/ + + +async def read(): +    # Fetch an instance, without loading a foreign key relationship on it. +    # Django style +    book = await Book.objects.get(title="The Hobbit") +    # or python style +    book = await Book.objects.get(Book.title == "The Hobbit") +    book2 = await Book.objects.first() + +    # first() fetch the instance with lower primary key value +    assert book == book2 + +    # you can access all fields on loaded model +    assert book.title == "The Hobbit" +    assert book.year == 1937 + +    # when no condition is passed to get() +    # it behaves as last() based on primary key column +    book3 = await Book.objects.get() +    assert book3.title == "The Tower of Fools" + +    # When you have a relation, ormar always defines a related model for you +    # even when all you loaded is a foreign key value like in this example +    assert isinstance(book.author, Author) +    # primary key is populated from foreign key stored in books table +    assert book.author.pk == 1 +    # since the related model was not loaded all other fields are None +    assert book.author.name is None + +    # Load the relationship from the database when you already have the related model +    # alternatively see joins section below +    await book.author.load() +    assert book.author.name == "J.R.R. Tolkien" + +    # get all rows for given model +    authors = await Author.objects.all() +    assert len(authors) == 2 + +    # to read more about reading data from the database +    # visit: https://collerek.github.io/ormar/queries/read/ + + +async def update(): +    # read existing row from db +    tolkien = await Author.objects.get(name="J.R.R. Tolkien") +    assert tolkien.name == "J.R.R. Tolkien" +    tolkien_id = tolkien.id + +    # change the selected property +    tolkien.name = "John Ronald Reuel Tolkien" +    # call update on a model instance +    await tolkien.update() + +    # confirm that object was updated +    tolkien = await Author.objects.get(name="John Ronald Reuel Tolkien") +    assert tolkien.name == "John Ronald Reuel Tolkien" +    assert tolkien.id == tolkien_id + +    # alternatively update data without loading +    await Author.objects.filter(name__contains="Tolkien").update(name="J.R.R. Tolkien") + +    # to read more about updating data in the database +    # visit: https://collerek.github.io/ormar/queries/update/ + + +async def delete(): +    silmarillion = await Book.objects.get(year=1977) +    # call delete() on instance +    await silmarillion.delete() + +    # alternatively delete without loading +    await Book.objects.delete(title="The Tower of Fools") + +    # note that when there is no record ormar raises NoMatch exception +    try: +        await Book.objects.get(year=1977) +    except ormar.NoMatch: +        print("No book from 1977!") + +    # to read more about deleting data from the database +    # visit: https://collerek.github.io/ormar/queries/delete/ + +    # note that despite the fact that record no longer exists in database +    # the object above is still accessible and you can use it (and i.e. save()) again. +    tolkien = silmarillion.author +    await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977) + + +async def joins(): +    # Tho join two models use select_related + +    # Django style +    book = await Book.objects.select_related("author").get(title="The Hobbit") +    # Python style +    book = await Book.objects.select_related(Book.author).get( +        Book.title == "The Hobbit" +    ) + +    # now the author is already prefetched +    assert book.author.name == "J.R.R. Tolkien" + +    # By default you also get a second side of the relation +    # constructed as lowercase source model name +'s' (books in this case) +    # you can also provide custom name with parameter related_name + +    # Django style +    author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien") +    # Python style +    author = await Author.objects.select_related(Author.books).all( +        Author.name == "J.R.R. Tolkien" +    ) +    assert len(author[0].books) == 3 + +    # for reverse and many to many relations you can also prefetch_related +    # that executes a separate query for each of related models + +    # Django style +    author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien") +    # Python style +    author = await Author.objects.prefetch_related(Author.books).get( +        Author.name == "J.R.R. Tolkien" +    ) +    assert len(author.books) == 3 + +    # to read more about relations +    # visit: https://collerek.github.io/ormar/relations/ + +    # to read more about joins and subqueries +    # visit: https://collerek.github.io/ormar/queries/joins-and-subqueries/ + + +async def filter_and_sort(): +    # to filter the query you can use filter() or pass key-value pars to +    # get(), all() etc. +    # to use special methods or access related model fields use double +    # underscore like to filter by the name of the author use author__name +    # Django style +    books = await Book.objects.all(author__name="J.R.R. Tolkien") +    # python style +    books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien") +    assert len(books) == 3 + +    # filter can accept special methods also separated with double underscore +    # to issue sql query ` where authors.name like "%tolkien%"` that is not +    # case sensitive (hence small t in Tolkien) +    # Django style +    books = await Book.objects.filter(author__name__icontains="tolkien").all() +    # python style +    books = await Book.objects.filter(Book.author.name.icontains("tolkien")).all() +    assert len(books) == 3 + +    # to sort use order_by() function of queryset +    # to sort decreasing use hyphen before the field name +    # same as with filter you can use double underscores to access related fields +    # Django style +    books = ( +        await Book.objects.filter(author__name__icontains="tolkien") +        .order_by("-year") +        .all() +    ) +    # python style +    books = ( +        await Book.objects.filter(Book.author.name.icontains("tolkien")) +        .order_by(Book.year.desc()) +        .all() +    ) +    assert len(books) == 3 +    assert books[0].title == "The Silmarillion" +    assert books[2].title == "The Hobbit" + +    # to read more about filtering and ordering +    # visit: https://collerek.github.io/ormar/queries/filter-and-sort/ + + +async def subset_of_columns(): +    # to exclude some columns from loading when querying the database +    # you can use fileds() method +    hobbit = await Book.objects.fields(["title"]).get(title="The Hobbit") +    # note that fields not included in fields are empty (set to None) +    assert hobbit.year is None +    assert hobbit.author is None + +    # selected field is there +    assert hobbit.title == "The Hobbit" + +    # alternatively you can provide columns you want to exclude +    hobbit = await Book.objects.exclude_fields(["year"]).get(title="The Hobbit") +    # year is still not set +    assert hobbit.year is None +    # but author is back +    assert hobbit.author is not None + +    # also you cannot exclude primary key column - it's always there +    # even if you EXPLICITLY exclude it it will be there + +    # note that each model have a shortcut for primary_key column which is pk +    # and you can filter/access/set the values by this alias like below +    assert hobbit.pk is not None + +    # note that you cannot exclude fields that are not nullable +    # (required) in model definition +    try: +        await Book.objects.exclude_fields(["title"]).get(title="The Hobbit") +    except pydantic.ValidationError: +        print("Cannot exclude non nullable field title") + +    # to read more about selecting subset of columns +    # visit: https://collerek.github.io/ormar/queries/select-columns/ + + +async def pagination(): +    # to limit number of returned rows use limit() +    books = await Book.objects.limit(1).all() +    assert len(books) == 1 +    assert books[0].title == "The Hobbit" + +    # to offset number of returned rows use offset() +    books = await Book.objects.limit(1).offset(1).all() +    assert len(books) == 1 +    assert books[0].title == "The Lord of the Rings" + +    # alternatively use paginate that combines both +    books = await Book.objects.paginate(page=2, page_size=2).all() +    assert len(books) == 2 +    # note that we removed one book of Sapkowski in delete() +    # and recreated The Silmarillion - by default when no order_by is set +    # ordering sorts by primary_key column +    assert books[0].title == "The Witcher" +    assert books[1].title == "The Silmarillion" + +    # to read more about pagination and number of rows +    # visit: https://collerek.github.io/ormar/queries/pagination-and-rows-number/ + + +async def aggregations(): +    # count: +    assert 2 == await Author.objects.count() + +    # exists +    assert await Book.objects.filter(title="The Hobbit").exists() + +    # maximum +    assert 1990 == await Book.objects.max(columns=["year"]) + +    # minimum +    assert 1937 == await Book.objects.min(columns=["year"]) + +    # average +    assert 1964.75 == await Book.objects.avg(columns=["year"]) + +    # sum +    assert 7859 == await Book.objects.sum(columns=["year"]) + +    # to read more about aggregated functions +    # visit: https://collerek.github.io/ormar/queries/aggregations/ + + +async def raw_data(): +    # extract raw data in a form of dicts or tuples +    # note that this skips the validation(!) as models are +    # not created from parsed data + +    # get list of objects as dicts +    assert await Book.objects.values() == [ +        {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}, +        {"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955}, +        {"id": 4, "author": 2, "title": "The Witcher", "year": 1990}, +        {"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977}, +    ] + +    # get list of objects as tuples +    assert await Book.objects.values_list() == [ +        (1, 1, "The Hobbit", 1937), +        (2, 1, "The Lord of the Rings", 1955), +        (4, 2, "The Witcher", 1990), +        (5, 1, "The Silmarillion", 1977), +    ] + +    # filter data - note how you always get a list +    assert await Book.objects.filter(title="The Hobbit").values() == [ +        {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937} +    ] + +    # select only wanted fields +    assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [ +        {"id": 1, "title": "The Hobbit"} +    ] + +    # if you select only one column you could flatten it with values_list +    assert await Book.objects.values_list("title", flatten=True) == [ +        "The Hobbit", +        "The Lord of the Rings", +        "The Witcher", +        "The Silmarillion", +    ] + +    # to read more about extracting raw values +    # visit: https://collerek.github.io/ormar/queries/aggregations/ + + +async def with_connect(function): +    # note that for any other backend than sqlite you actually need to +    # connect to the database to perform db operations +    async with database: +        await function() + +    # note that if you use framework like `fastapi` you shouldn't connect +    # in your endpoints but have a global connection pool +    # check https://collerek.github.io/ormar/fastapi/ and section with db connection + + +# gather and execute all functions +# note - normally import should be at the beginning of the file +import asyncio + +# note that normally you use gather() function to run several functions +# concurrently but we actually modify the data and we rely on the order of functions +for func in [ +    create, +    read, +    update, +    delete, +    joins, +    filter_and_sort, +    subset_of_columns, +    pagination, +    aggregations, +    raw_data, +]: +    print(f"Executing: {func.__name__}") +    asyncio.run(with_connect(func)) + +# drop the database tables +metadata.drop_all(engine) +``` + +## Ormar Specification + +### QuerySet methods + +*  `create(**kwargs): -> Model` +*  `get(*args, **kwargs): -> Model` +*  `get_or_none(*args, **kwargs): -> Optional[Model]` +*  `get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]` +*  `first(*args, **kwargs): -> Model` +*  `update(each: bool = False, **kwargs) -> int` +*  `update_or_create(**kwargs) -> Model` +*  `bulk_create(objects: List[Model]) -> None` +*  `bulk_update(objects: List[Model], columns: List[str] = None) -> None` +*  `delete(*args, each: bool = False, **kwargs) -> int` +*  `all(*args, **kwargs) -> List[Optional[Model]]` +*  `iterate(*args, **kwargs) -> AsyncGenerator[Model]` +*  `filter(*args, **kwargs) -> QuerySet` +*  `exclude(*args, **kwargs) -> QuerySet` +*  `select_related(related: Union[List, str]) -> QuerySet` +*  `prefetch_related(related: Union[List, str]) -> QuerySet` +*  `limit(limit_count: int) -> QuerySet` +*  `offset(offset: int) -> QuerySet` +*  `count(distinct: bool = True) -> int` +*  `exists() -> bool` +*  `max(columns: List[str]) -> Any` +*  `min(columns: List[str]) -> Any` +*  `avg(columns: List[str]) -> Any` +*  `sum(columns: List[str]) -> Any` +*  `fields(columns: Union[List, str, set, dict]) -> QuerySet` +*  `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` +*  `order_by(columns:Union[List, str]) -> QuerySet` +*  `values(fields: Union[List, str, Set, Dict])` +*  `values_list(fields: Union[List, str, Set, Dict])` + + +#### Relation types + +*  One to many  - with `ForeignKey(to: Model)` +*  Many to many - with `ManyToMany(to: Model, Optional[through]: Model)` + +#### Model fields types + +Available Model Fields (with required args - optional ones in docs): + +* `String(max_length)` +* `Text()` +* `Boolean()` +* `Integer()` +* `Float()` +* `Date()` +* `Time()` +* `DateTime()` +* `JSON()` +* `BigInteger()` +* `SmallInteger()` +* `Decimal(scale, precision)` +* `UUID()` +* `LargeBinary(max_length)` +* `Enum(enum_class)` +* `Enum` like Field - by passing `choices` to any other Field type +* `EncryptedString` - by passing `encrypt_secret` and `encrypt_backend` +* `ForeignKey(to)` +* `ManyToMany(to)` + +### Available fields options +The following keyword arguments are supported on all field types. + +* `primary_key: bool` +* `nullable: bool` +* `default: Any` +* `server_default: Any` +* `index: bool` +* `unique: bool` +* `choices: typing.Sequence` +* `name: str` +* `pydantic_only: bool` + +All fields are required unless one of the following is set: + +* `nullable` - Creates a nullable column. Sets the default to `False`. Read the fields common parameters for details. +* `sql_nullable` - Used to set different setting for pydantic and the database. Sets the default to `nullable` value. Read the fields common parameters for details. +* `default` - Set a default value for the field. **Not available for relation fields** +* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields** +* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column. +  Autoincrement is set by default on int primary keys. +* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database. + +### Available signals + +Signals allow to trigger your function for a given event on a given Model. + +* `pre_save` +* `post_save` +* `pre_update` +* `post_update` +* `pre_delete` +* `post_delete` +* `pre_relation_add` +* `post_relation_add` +* `pre_relation_remove` +* `post_relation_remove` +* `post_bulk_update` + + +[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ +[databases]: https://github.com/encode/databases +[pydantic]: https://pydantic-docs.helpmanual.io/ +[encode/orm]: https://github.com/encode/orm/ +[alembic]: https://alembic.sqlalchemy.org/en/latest/ +[fastapi]: https://fastapi.tiangolo.com/ +[documentation]: https://collerek.github.io/ormar/ +[migrations]: https://collerek.github.io/ormar/models/migrations/ +[asyncio]: https://docs.python.org/3/library/asyncio.html +[releases]: https://collerek.github.io/ormar/releases/ +[tests]: https://github.com/collerek/ormar/tree/master/tests + + +%package -n python3-ormar +Summary:	A simple async ORM with fastapi in mind and pydantic validation. +Provides:	python-ormar +BuildRequires:	python3-devel +BuildRequires:	python3-setuptools +BuildRequires:	python3-pip +%description -n python3-ormar +# ormar +<p> +<a href="https://pypi.org/project/ormar"> +    <img src="https://img.shields.io/pypi/v/ormar.svg" alt="Pypi version"> +</a> +<a href="https://pypi.org/project/ormar"> +    <img src="https://img.shields.io/pypi/pyversions/ormar.svg" alt="Pypi version"> +</a> +<img src="https://github.com/collerek/ormar/workflows/build/badge.svg" alt="Build Status"> +<a href="https://codecov.io/gh/collerek/ormar"> +    <img src="https://codecov.io/gh/collerek/ormar/branch/master/graph/badge.svg" alt="Coverage"> +</a> +<a href="https://www.codefactor.io/repository/github/collerek/ormar"> +<img src="https://www.codefactor.io/repository/github/collerek/ormar/badge" alt="CodeFactor" /> +</a> +<a href="https://codeclimate.com/github/collerek/ormar/maintainability"> +<img src="https://api.codeclimate.com/v1/badges/186bc79245724864a7aa/maintainability" /></a> +<a href="https://pepy.tech/project/ormar"> +<img src="https://pepy.tech/badge/ormar"></a> +</p> + +### Overview + +The `ormar` package is an async mini ORM for Python, with support for **Postgres, +MySQL**, and **SQLite**. + +The main benefits of using `ormar` are: + +*  getting an **async ORM that can be used with async frameworks** (fastapi, starlette etc.) +*  getting just **one model to maintain** - you don't have to maintain pydantic and other orm models (sqlalchemy, peewee, gino etc.) + +The goal was to create a simple ORM that can be **used directly (as request and response models) with [`fastapi`][fastapi]** that bases it's data validation on pydantic. + +Ormar - apart from the obvious "ORM" in name - gets its name from _ormar_ in Swedish which means _snakes_, and _ormar_ in Croatian which means _cabinet_. + +And what's a better name for python ORM than snakes cabinet :) + +**If you like ormar remember to star the repository in [github](https://github.com/collerek/ormar)!** + +The bigger community we build, the easier it will be to catch bugs and attract contributors ;) + +### Documentation + +Check out the [documentation][documentation] for details. + +**Note that for brevity most of the documentation snippets omit the creation of the database +and scheduling the execution of functions for asynchronous run.** + +If you want more real life examples than in the documentation you can see the [tests][tests] folder, +since they actually have to create and connect to a database in most of the tests. + +Yet remember that those are - well - tests and not all solutions are suitable to be used in real life applications. + +### Part of the `fastapi` ecosystem + +As part of the fastapi ecosystem `ormar` is supported in libraries that somehow work with databases. + +As of now `ormar` is supported by: + +*  [`fastapi-users`](https://github.com/frankie567/fastapi-users) +*  [`fastapi-crudrouter`](https://github.com/awtkns/fastapi-crudrouter) +*  [`fastapi-pagination`](https://github.com/uriyyo/fastapi-pagination) + +If you maintain or use a different library and would like it to support `ormar` let us know how we can help. + +### Dependencies + +Ormar is built with: + +* [`sqlalchemy core`][sqlalchemy-core] for query building. +* [`databases`][databases] for cross-database async support. +* [`pydantic`][pydantic] for data validation. +* `typing_extensions` for python 3.6 - 3.7 + +### License + +`ormar` is built as open-sorce software and will remain completely free (MIT license). + +As I write open-source code to solve everyday problems in my work or to promote and build strong python +community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained. + +<a aria-label="Sponsor collerek" href="https://github.com/sponsors/collerek" style="text-decoration: none; color: #c9d1d9 !important;"> +<div style=" +    background-color: #21262d; +    border-color: #30363d; +    box-shadow:  0 0 transparent, 0 0 transparent; +    color: #c9d1d9 !important; +    border: 1px solid; +    border-radius: 6px; +    cursor: pointer; +    display: inline-block; +    font-size: 14px; +    padding: 10px; +    line-height: 0px; +    height: 40px; +"> +<span style="color: #c9d1d9 !important;">Sponsor - Github Sponsors</span> +</div> +</a> + +### Migrating from `sqlalchemy` and existing databases + +If you currently use `sqlalchemy` and would like to switch to `ormar` check out the auto-translation +tool that can help you with translating existing sqlalchemy orm models so you do not have to do it manually. + +**Beta** versions available at github: [`sqlalchemy-to-ormar`](https://github.com/collerek/sqlalchemy-to-ormar) +or simply `pip install sqlalchemy-to-ormar` + +`sqlalchemy-to-ormar` can be used in pair with `sqlacodegen` to auto-map/ generate `ormar` models from existing database, even if you don't use `sqlalchemy` for your project. + +### Migrations & Database creation + +Because ormar is built on SQLAlchemy core, you can use [`alembic`][alembic] to provide +database migrations (and you really should for production code). + +For tests and basic applications the `sqlalchemy` is more than enough: +```python +# note this is just a partial snippet full working example below +# 1. Imports +import sqlalchemy +import databases + +# 2. Initialization +DATABASE_URL = "sqlite:///db.sqlite" +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() + +# Define models here + +# 3. Database creation and tables creation +engine = sqlalchemy.create_engine(DATABASE_URL) +metadata.create_all(engine) +``` + +For a sample configuration of alembic and more information regarding migrations and +database creation visit [migrations][migrations] documentation section. + +### Package versions +**ormar is still under development:** +We recommend pinning any dependencies (with i.e. `ormar~=0.9.1`) + +`ormar` also follows the release numeration that breaking changes bump the major number, +while other changes and fixes bump minor number, so with the latter you should be safe to +update, yet always read the [releases][releases] docs before. +`example: (0.5.2 -> 0.6.0 - breaking, 0.5.2 -> 0.5.3 - non breaking)`. + +### Asynchronous Python + +Note that `ormar` is an asynchronous ORM, which means that you have to `await` the calls to +the methods, that are scheduled for execution in an event loop. Python has a builtin module +[`asyncio`][asyncio] that allows you to do just that. + +Note that most "normal" python interpreters do not allow execution of `await` +outside of a function (because you actually schedule this function for delayed execution +and don't get the result immediately). + +In a modern web framework (like `fastapi`), the framework will handle this for you, but if +you plan to do this on your own you need to perform this manually like described in the +quick start below. + +### Quick Start + +Note that you can find the same script in examples folder on github. + +```python +from typing import Optional + +import databases +import pydantic + +import ormar +import sqlalchemy + +DATABASE_URL = "sqlite:///db.sqlite" +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() + + +# note that this step is optional -> all ormar cares is a internal +# class with name Meta and proper parameters, but this way you do not +# have to repeat the same parameters if you use only one database +class BaseMeta(ormar.ModelMeta): +    metadata = metadata +    database = database + + +# Note that all type hints are optional +# below is a perfectly valid model declaration +# class Author(ormar.Model): +#     class Meta(BaseMeta): +#         tablename = "authors" +# +#     id = ormar.Integer(primary_key=True) # <= notice no field types +#     name = ormar.String(max_length=100) + + +class Author(ormar.Model): +    class Meta(BaseMeta): +        tablename = "authors" + +    id: int = ormar.Integer(primary_key=True) +    name: str = ormar.String(max_length=100) + + +class Book(ormar.Model): +    class Meta(BaseMeta): +        tablename = "books" + +    id: int = ormar.Integer(primary_key=True) +    author: Optional[Author] = ormar.ForeignKey(Author) +    title: str = ormar.String(max_length=100) +    year: int = ormar.Integer(nullable=True) + + +# create the database +# note that in production you should use migrations +# note that this is not required if you connect to existing database +engine = sqlalchemy.create_engine(DATABASE_URL) +# just to be sure we clear the db before +metadata.drop_all(engine) +metadata.create_all(engine) + + +# all functions below are divided into functionality categories +# note how all functions are defined with async - hence can use await AND needs to +# be awaited on their own +async def create(): +    # Create some records to work with through QuerySet.create method. +    # Note that queryset is exposed on each Model's class as objects +    tolkien = await Author.objects.create(name="J.R.R. Tolkien") +    await Book.objects.create(author=tolkien, title="The Hobbit", year=1937) +    await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955) +    await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977) + +    # alternative creation of object divided into 2 steps +    sapkowski = Author(name="Andrzej Sapkowski") +    # do some stuff +    await sapkowski.save() + +    # or save() after initialization +    await Book(author=sapkowski, title="The Witcher", year=1990).save() +    await Book(author=sapkowski, title="The Tower of Fools", year=2002).save() + +    # to read more about inserting data into the database +    # visit: https://collerek.github.io/ormar/queries/create/ + + +async def read(): +    # Fetch an instance, without loading a foreign key relationship on it. +    # Django style +    book = await Book.objects.get(title="The Hobbit") +    # or python style +    book = await Book.objects.get(Book.title == "The Hobbit") +    book2 = await Book.objects.first() + +    # first() fetch the instance with lower primary key value +    assert book == book2 + +    # you can access all fields on loaded model +    assert book.title == "The Hobbit" +    assert book.year == 1937 + +    # when no condition is passed to get() +    # it behaves as last() based on primary key column +    book3 = await Book.objects.get() +    assert book3.title == "The Tower of Fools" + +    # When you have a relation, ormar always defines a related model for you +    # even when all you loaded is a foreign key value like in this example +    assert isinstance(book.author, Author) +    # primary key is populated from foreign key stored in books table +    assert book.author.pk == 1 +    # since the related model was not loaded all other fields are None +    assert book.author.name is None + +    # Load the relationship from the database when you already have the related model +    # alternatively see joins section below +    await book.author.load() +    assert book.author.name == "J.R.R. Tolkien" + +    # get all rows for given model +    authors = await Author.objects.all() +    assert len(authors) == 2 + +    # to read more about reading data from the database +    # visit: https://collerek.github.io/ormar/queries/read/ + + +async def update(): +    # read existing row from db +    tolkien = await Author.objects.get(name="J.R.R. Tolkien") +    assert tolkien.name == "J.R.R. Tolkien" +    tolkien_id = tolkien.id + +    # change the selected property +    tolkien.name = "John Ronald Reuel Tolkien" +    # call update on a model instance +    await tolkien.update() + +    # confirm that object was updated +    tolkien = await Author.objects.get(name="John Ronald Reuel Tolkien") +    assert tolkien.name == "John Ronald Reuel Tolkien" +    assert tolkien.id == tolkien_id + +    # alternatively update data without loading +    await Author.objects.filter(name__contains="Tolkien").update(name="J.R.R. Tolkien") + +    # to read more about updating data in the database +    # visit: https://collerek.github.io/ormar/queries/update/ + + +async def delete(): +    silmarillion = await Book.objects.get(year=1977) +    # call delete() on instance +    await silmarillion.delete() + +    # alternatively delete without loading +    await Book.objects.delete(title="The Tower of Fools") + +    # note that when there is no record ormar raises NoMatch exception +    try: +        await Book.objects.get(year=1977) +    except ormar.NoMatch: +        print("No book from 1977!") + +    # to read more about deleting data from the database +    # visit: https://collerek.github.io/ormar/queries/delete/ + +    # note that despite the fact that record no longer exists in database +    # the object above is still accessible and you can use it (and i.e. save()) again. +    tolkien = silmarillion.author +    await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977) + + +async def joins(): +    # Tho join two models use select_related + +    # Django style +    book = await Book.objects.select_related("author").get(title="The Hobbit") +    # Python style +    book = await Book.objects.select_related(Book.author).get( +        Book.title == "The Hobbit" +    ) + +    # now the author is already prefetched +    assert book.author.name == "J.R.R. Tolkien" + +    # By default you also get a second side of the relation +    # constructed as lowercase source model name +'s' (books in this case) +    # you can also provide custom name with parameter related_name + +    # Django style +    author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien") +    # Python style +    author = await Author.objects.select_related(Author.books).all( +        Author.name == "J.R.R. Tolkien" +    ) +    assert len(author[0].books) == 3 + +    # for reverse and many to many relations you can also prefetch_related +    # that executes a separate query for each of related models + +    # Django style +    author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien") +    # Python style +    author = await Author.objects.prefetch_related(Author.books).get( +        Author.name == "J.R.R. Tolkien" +    ) +    assert len(author.books) == 3 + +    # to read more about relations +    # visit: https://collerek.github.io/ormar/relations/ + +    # to read more about joins and subqueries +    # visit: https://collerek.github.io/ormar/queries/joins-and-subqueries/ + + +async def filter_and_sort(): +    # to filter the query you can use filter() or pass key-value pars to +    # get(), all() etc. +    # to use special methods or access related model fields use double +    # underscore like to filter by the name of the author use author__name +    # Django style +    books = await Book.objects.all(author__name="J.R.R. Tolkien") +    # python style +    books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien") +    assert len(books) == 3 + +    # filter can accept special methods also separated with double underscore +    # to issue sql query ` where authors.name like "%tolkien%"` that is not +    # case sensitive (hence small t in Tolkien) +    # Django style +    books = await Book.objects.filter(author__name__icontains="tolkien").all() +    # python style +    books = await Book.objects.filter(Book.author.name.icontains("tolkien")).all() +    assert len(books) == 3 + +    # to sort use order_by() function of queryset +    # to sort decreasing use hyphen before the field name +    # same as with filter you can use double underscores to access related fields +    # Django style +    books = ( +        await Book.objects.filter(author__name__icontains="tolkien") +        .order_by("-year") +        .all() +    ) +    # python style +    books = ( +        await Book.objects.filter(Book.author.name.icontains("tolkien")) +        .order_by(Book.year.desc()) +        .all() +    ) +    assert len(books) == 3 +    assert books[0].title == "The Silmarillion" +    assert books[2].title == "The Hobbit" + +    # to read more about filtering and ordering +    # visit: https://collerek.github.io/ormar/queries/filter-and-sort/ + + +async def subset_of_columns(): +    # to exclude some columns from loading when querying the database +    # you can use fileds() method +    hobbit = await Book.objects.fields(["title"]).get(title="The Hobbit") +    # note that fields not included in fields are empty (set to None) +    assert hobbit.year is None +    assert hobbit.author is None + +    # selected field is there +    assert hobbit.title == "The Hobbit" + +    # alternatively you can provide columns you want to exclude +    hobbit = await Book.objects.exclude_fields(["year"]).get(title="The Hobbit") +    # year is still not set +    assert hobbit.year is None +    # but author is back +    assert hobbit.author is not None + +    # also you cannot exclude primary key column - it's always there +    # even if you EXPLICITLY exclude it it will be there + +    # note that each model have a shortcut for primary_key column which is pk +    # and you can filter/access/set the values by this alias like below +    assert hobbit.pk is not None + +    # note that you cannot exclude fields that are not nullable +    # (required) in model definition +    try: +        await Book.objects.exclude_fields(["title"]).get(title="The Hobbit") +    except pydantic.ValidationError: +        print("Cannot exclude non nullable field title") + +    # to read more about selecting subset of columns +    # visit: https://collerek.github.io/ormar/queries/select-columns/ + + +async def pagination(): +    # to limit number of returned rows use limit() +    books = await Book.objects.limit(1).all() +    assert len(books) == 1 +    assert books[0].title == "The Hobbit" + +    # to offset number of returned rows use offset() +    books = await Book.objects.limit(1).offset(1).all() +    assert len(books) == 1 +    assert books[0].title == "The Lord of the Rings" + +    # alternatively use paginate that combines both +    books = await Book.objects.paginate(page=2, page_size=2).all() +    assert len(books) == 2 +    # note that we removed one book of Sapkowski in delete() +    # and recreated The Silmarillion - by default when no order_by is set +    # ordering sorts by primary_key column +    assert books[0].title == "The Witcher" +    assert books[1].title == "The Silmarillion" + +    # to read more about pagination and number of rows +    # visit: https://collerek.github.io/ormar/queries/pagination-and-rows-number/ + + +async def aggregations(): +    # count: +    assert 2 == await Author.objects.count() + +    # exists +    assert await Book.objects.filter(title="The Hobbit").exists() + +    # maximum +    assert 1990 == await Book.objects.max(columns=["year"]) + +    # minimum +    assert 1937 == await Book.objects.min(columns=["year"]) + +    # average +    assert 1964.75 == await Book.objects.avg(columns=["year"]) + +    # sum +    assert 7859 == await Book.objects.sum(columns=["year"]) + +    # to read more about aggregated functions +    # visit: https://collerek.github.io/ormar/queries/aggregations/ + + +async def raw_data(): +    # extract raw data in a form of dicts or tuples +    # note that this skips the validation(!) as models are +    # not created from parsed data + +    # get list of objects as dicts +    assert await Book.objects.values() == [ +        {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}, +        {"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955}, +        {"id": 4, "author": 2, "title": "The Witcher", "year": 1990}, +        {"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977}, +    ] + +    # get list of objects as tuples +    assert await Book.objects.values_list() == [ +        (1, 1, "The Hobbit", 1937), +        (2, 1, "The Lord of the Rings", 1955), +        (4, 2, "The Witcher", 1990), +        (5, 1, "The Silmarillion", 1977), +    ] + +    # filter data - note how you always get a list +    assert await Book.objects.filter(title="The Hobbit").values() == [ +        {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937} +    ] + +    # select only wanted fields +    assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [ +        {"id": 1, "title": "The Hobbit"} +    ] + +    # if you select only one column you could flatten it with values_list +    assert await Book.objects.values_list("title", flatten=True) == [ +        "The Hobbit", +        "The Lord of the Rings", +        "The Witcher", +        "The Silmarillion", +    ] + +    # to read more about extracting raw values +    # visit: https://collerek.github.io/ormar/queries/aggregations/ + + +async def with_connect(function): +    # note that for any other backend than sqlite you actually need to +    # connect to the database to perform db operations +    async with database: +        await function() + +    # note that if you use framework like `fastapi` you shouldn't connect +    # in your endpoints but have a global connection pool +    # check https://collerek.github.io/ormar/fastapi/ and section with db connection + + +# gather and execute all functions +# note - normally import should be at the beginning of the file +import asyncio + +# note that normally you use gather() function to run several functions +# concurrently but we actually modify the data and we rely on the order of functions +for func in [ +    create, +    read, +    update, +    delete, +    joins, +    filter_and_sort, +    subset_of_columns, +    pagination, +    aggregations, +    raw_data, +]: +    print(f"Executing: {func.__name__}") +    asyncio.run(with_connect(func)) + +# drop the database tables +metadata.drop_all(engine) +``` + +## Ormar Specification + +### QuerySet methods + +*  `create(**kwargs): -> Model` +*  `get(*args, **kwargs): -> Model` +*  `get_or_none(*args, **kwargs): -> Optional[Model]` +*  `get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]` +*  `first(*args, **kwargs): -> Model` +*  `update(each: bool = False, **kwargs) -> int` +*  `update_or_create(**kwargs) -> Model` +*  `bulk_create(objects: List[Model]) -> None` +*  `bulk_update(objects: List[Model], columns: List[str] = None) -> None` +*  `delete(*args, each: bool = False, **kwargs) -> int` +*  `all(*args, **kwargs) -> List[Optional[Model]]` +*  `iterate(*args, **kwargs) -> AsyncGenerator[Model]` +*  `filter(*args, **kwargs) -> QuerySet` +*  `exclude(*args, **kwargs) -> QuerySet` +*  `select_related(related: Union[List, str]) -> QuerySet` +*  `prefetch_related(related: Union[List, str]) -> QuerySet` +*  `limit(limit_count: int) -> QuerySet` +*  `offset(offset: int) -> QuerySet` +*  `count(distinct: bool = True) -> int` +*  `exists() -> bool` +*  `max(columns: List[str]) -> Any` +*  `min(columns: List[str]) -> Any` +*  `avg(columns: List[str]) -> Any` +*  `sum(columns: List[str]) -> Any` +*  `fields(columns: Union[List, str, set, dict]) -> QuerySet` +*  `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` +*  `order_by(columns:Union[List, str]) -> QuerySet` +*  `values(fields: Union[List, str, Set, Dict])` +*  `values_list(fields: Union[List, str, Set, Dict])` + + +#### Relation types + +*  One to many  - with `ForeignKey(to: Model)` +*  Many to many - with `ManyToMany(to: Model, Optional[through]: Model)` + +#### Model fields types + +Available Model Fields (with required args - optional ones in docs): + +* `String(max_length)` +* `Text()` +* `Boolean()` +* `Integer()` +* `Float()` +* `Date()` +* `Time()` +* `DateTime()` +* `JSON()` +* `BigInteger()` +* `SmallInteger()` +* `Decimal(scale, precision)` +* `UUID()` +* `LargeBinary(max_length)` +* `Enum(enum_class)` +* `Enum` like Field - by passing `choices` to any other Field type +* `EncryptedString` - by passing `encrypt_secret` and `encrypt_backend` +* `ForeignKey(to)` +* `ManyToMany(to)` + +### Available fields options +The following keyword arguments are supported on all field types. + +* `primary_key: bool` +* `nullable: bool` +* `default: Any` +* `server_default: Any` +* `index: bool` +* `unique: bool` +* `choices: typing.Sequence` +* `name: str` +* `pydantic_only: bool` + +All fields are required unless one of the following is set: + +* `nullable` - Creates a nullable column. Sets the default to `False`. Read the fields common parameters for details. +* `sql_nullable` - Used to set different setting for pydantic and the database. Sets the default to `nullable` value. Read the fields common parameters for details. +* `default` - Set a default value for the field. **Not available for relation fields** +* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields** +* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column. +  Autoincrement is set by default on int primary keys. +* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database. + +### Available signals + +Signals allow to trigger your function for a given event on a given Model. + +* `pre_save` +* `post_save` +* `pre_update` +* `post_update` +* `pre_delete` +* `post_delete` +* `pre_relation_add` +* `post_relation_add` +* `pre_relation_remove` +* `post_relation_remove` +* `post_bulk_update` + + +[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ +[databases]: https://github.com/encode/databases +[pydantic]: https://pydantic-docs.helpmanual.io/ +[encode/orm]: https://github.com/encode/orm/ +[alembic]: https://alembic.sqlalchemy.org/en/latest/ +[fastapi]: https://fastapi.tiangolo.com/ +[documentation]: https://collerek.github.io/ormar/ +[migrations]: https://collerek.github.io/ormar/models/migrations/ +[asyncio]: https://docs.python.org/3/library/asyncio.html +[releases]: https://collerek.github.io/ormar/releases/ +[tests]: https://github.com/collerek/ormar/tree/master/tests + + +%package help +Summary:	Development documents and examples for ormar +Provides:	python3-ormar-doc +%description help +# ormar +<p> +<a href="https://pypi.org/project/ormar"> +    <img src="https://img.shields.io/pypi/v/ormar.svg" alt="Pypi version"> +</a> +<a href="https://pypi.org/project/ormar"> +    <img src="https://img.shields.io/pypi/pyversions/ormar.svg" alt="Pypi version"> +</a> +<img src="https://github.com/collerek/ormar/workflows/build/badge.svg" alt="Build Status"> +<a href="https://codecov.io/gh/collerek/ormar"> +    <img src="https://codecov.io/gh/collerek/ormar/branch/master/graph/badge.svg" alt="Coverage"> +</a> +<a href="https://www.codefactor.io/repository/github/collerek/ormar"> +<img src="https://www.codefactor.io/repository/github/collerek/ormar/badge" alt="CodeFactor" /> +</a> +<a href="https://codeclimate.com/github/collerek/ormar/maintainability"> +<img src="https://api.codeclimate.com/v1/badges/186bc79245724864a7aa/maintainability" /></a> +<a href="https://pepy.tech/project/ormar"> +<img src="https://pepy.tech/badge/ormar"></a> +</p> + +### Overview + +The `ormar` package is an async mini ORM for Python, with support for **Postgres, +MySQL**, and **SQLite**. + +The main benefits of using `ormar` are: + +*  getting an **async ORM that can be used with async frameworks** (fastapi, starlette etc.) +*  getting just **one model to maintain** - you don't have to maintain pydantic and other orm models (sqlalchemy, peewee, gino etc.) + +The goal was to create a simple ORM that can be **used directly (as request and response models) with [`fastapi`][fastapi]** that bases it's data validation on pydantic. + +Ormar - apart from the obvious "ORM" in name - gets its name from _ormar_ in Swedish which means _snakes_, and _ormar_ in Croatian which means _cabinet_. + +And what's a better name for python ORM than snakes cabinet :) + +**If you like ormar remember to star the repository in [github](https://github.com/collerek/ormar)!** + +The bigger community we build, the easier it will be to catch bugs and attract contributors ;) + +### Documentation + +Check out the [documentation][documentation] for details. + +**Note that for brevity most of the documentation snippets omit the creation of the database +and scheduling the execution of functions for asynchronous run.** + +If you want more real life examples than in the documentation you can see the [tests][tests] folder, +since they actually have to create and connect to a database in most of the tests. + +Yet remember that those are - well - tests and not all solutions are suitable to be used in real life applications. + +### Part of the `fastapi` ecosystem + +As part of the fastapi ecosystem `ormar` is supported in libraries that somehow work with databases. + +As of now `ormar` is supported by: + +*  [`fastapi-users`](https://github.com/frankie567/fastapi-users) +*  [`fastapi-crudrouter`](https://github.com/awtkns/fastapi-crudrouter) +*  [`fastapi-pagination`](https://github.com/uriyyo/fastapi-pagination) + +If you maintain or use a different library and would like it to support `ormar` let us know how we can help. + +### Dependencies + +Ormar is built with: + +* [`sqlalchemy core`][sqlalchemy-core] for query building. +* [`databases`][databases] for cross-database async support. +* [`pydantic`][pydantic] for data validation. +* `typing_extensions` for python 3.6 - 3.7 + +### License + +`ormar` is built as open-sorce software and will remain completely free (MIT license). + +As I write open-source code to solve everyday problems in my work or to promote and build strong python +community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained. + +<a aria-label="Sponsor collerek" href="https://github.com/sponsors/collerek" style="text-decoration: none; color: #c9d1d9 !important;"> +<div style=" +    background-color: #21262d; +    border-color: #30363d; +    box-shadow:  0 0 transparent, 0 0 transparent; +    color: #c9d1d9 !important; +    border: 1px solid; +    border-radius: 6px; +    cursor: pointer; +    display: inline-block; +    font-size: 14px; +    padding: 10px; +    line-height: 0px; +    height: 40px; +"> +<span style="color: #c9d1d9 !important;">Sponsor - Github Sponsors</span> +</div> +</a> + +### Migrating from `sqlalchemy` and existing databases + +If you currently use `sqlalchemy` and would like to switch to `ormar` check out the auto-translation +tool that can help you with translating existing sqlalchemy orm models so you do not have to do it manually. + +**Beta** versions available at github: [`sqlalchemy-to-ormar`](https://github.com/collerek/sqlalchemy-to-ormar) +or simply `pip install sqlalchemy-to-ormar` + +`sqlalchemy-to-ormar` can be used in pair with `sqlacodegen` to auto-map/ generate `ormar` models from existing database, even if you don't use `sqlalchemy` for your project. + +### Migrations & Database creation + +Because ormar is built on SQLAlchemy core, you can use [`alembic`][alembic] to provide +database migrations (and you really should for production code). + +For tests and basic applications the `sqlalchemy` is more than enough: +```python +# note this is just a partial snippet full working example below +# 1. Imports +import sqlalchemy +import databases + +# 2. Initialization +DATABASE_URL = "sqlite:///db.sqlite" +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() + +# Define models here + +# 3. Database creation and tables creation +engine = sqlalchemy.create_engine(DATABASE_URL) +metadata.create_all(engine) +``` + +For a sample configuration of alembic and more information regarding migrations and +database creation visit [migrations][migrations] documentation section. + +### Package versions +**ormar is still under development:** +We recommend pinning any dependencies (with i.e. `ormar~=0.9.1`) + +`ormar` also follows the release numeration that breaking changes bump the major number, +while other changes and fixes bump minor number, so with the latter you should be safe to +update, yet always read the [releases][releases] docs before. +`example: (0.5.2 -> 0.6.0 - breaking, 0.5.2 -> 0.5.3 - non breaking)`. + +### Asynchronous Python + +Note that `ormar` is an asynchronous ORM, which means that you have to `await` the calls to +the methods, that are scheduled for execution in an event loop. Python has a builtin module +[`asyncio`][asyncio] that allows you to do just that. + +Note that most "normal" python interpreters do not allow execution of `await` +outside of a function (because you actually schedule this function for delayed execution +and don't get the result immediately). + +In a modern web framework (like `fastapi`), the framework will handle this for you, but if +you plan to do this on your own you need to perform this manually like described in the +quick start below. + +### Quick Start + +Note that you can find the same script in examples folder on github. + +```python +from typing import Optional + +import databases +import pydantic + +import ormar +import sqlalchemy + +DATABASE_URL = "sqlite:///db.sqlite" +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() + + +# note that this step is optional -> all ormar cares is a internal +# class with name Meta and proper parameters, but this way you do not +# have to repeat the same parameters if you use only one database +class BaseMeta(ormar.ModelMeta): +    metadata = metadata +    database = database + + +# Note that all type hints are optional +# below is a perfectly valid model declaration +# class Author(ormar.Model): +#     class Meta(BaseMeta): +#         tablename = "authors" +# +#     id = ormar.Integer(primary_key=True) # <= notice no field types +#     name = ormar.String(max_length=100) + + +class Author(ormar.Model): +    class Meta(BaseMeta): +        tablename = "authors" + +    id: int = ormar.Integer(primary_key=True) +    name: str = ormar.String(max_length=100) + + +class Book(ormar.Model): +    class Meta(BaseMeta): +        tablename = "books" + +    id: int = ormar.Integer(primary_key=True) +    author: Optional[Author] = ormar.ForeignKey(Author) +    title: str = ormar.String(max_length=100) +    year: int = ormar.Integer(nullable=True) + + +# create the database +# note that in production you should use migrations +# note that this is not required if you connect to existing database +engine = sqlalchemy.create_engine(DATABASE_URL) +# just to be sure we clear the db before +metadata.drop_all(engine) +metadata.create_all(engine) + + +# all functions below are divided into functionality categories +# note how all functions are defined with async - hence can use await AND needs to +# be awaited on their own +async def create(): +    # Create some records to work with through QuerySet.create method. +    # Note that queryset is exposed on each Model's class as objects +    tolkien = await Author.objects.create(name="J.R.R. Tolkien") +    await Book.objects.create(author=tolkien, title="The Hobbit", year=1937) +    await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955) +    await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977) + +    # alternative creation of object divided into 2 steps +    sapkowski = Author(name="Andrzej Sapkowski") +    # do some stuff +    await sapkowski.save() + +    # or save() after initialization +    await Book(author=sapkowski, title="The Witcher", year=1990).save() +    await Book(author=sapkowski, title="The Tower of Fools", year=2002).save() + +    # to read more about inserting data into the database +    # visit: https://collerek.github.io/ormar/queries/create/ + + +async def read(): +    # Fetch an instance, without loading a foreign key relationship on it. +    # Django style +    book = await Book.objects.get(title="The Hobbit") +    # or python style +    book = await Book.objects.get(Book.title == "The Hobbit") +    book2 = await Book.objects.first() + +    # first() fetch the instance with lower primary key value +    assert book == book2 + +    # you can access all fields on loaded model +    assert book.title == "The Hobbit" +    assert book.year == 1937 + +    # when no condition is passed to get() +    # it behaves as last() based on primary key column +    book3 = await Book.objects.get() +    assert book3.title == "The Tower of Fools" + +    # When you have a relation, ormar always defines a related model for you +    # even when all you loaded is a foreign key value like in this example +    assert isinstance(book.author, Author) +    # primary key is populated from foreign key stored in books table +    assert book.author.pk == 1 +    # since the related model was not loaded all other fields are None +    assert book.author.name is None + +    # Load the relationship from the database when you already have the related model +    # alternatively see joins section below +    await book.author.load() +    assert book.author.name == "J.R.R. Tolkien" + +    # get all rows for given model +    authors = await Author.objects.all() +    assert len(authors) == 2 + +    # to read more about reading data from the database +    # visit: https://collerek.github.io/ormar/queries/read/ + + +async def update(): +    # read existing row from db +    tolkien = await Author.objects.get(name="J.R.R. Tolkien") +    assert tolkien.name == "J.R.R. Tolkien" +    tolkien_id = tolkien.id + +    # change the selected property +    tolkien.name = "John Ronald Reuel Tolkien" +    # call update on a model instance +    await tolkien.update() + +    # confirm that object was updated +    tolkien = await Author.objects.get(name="John Ronald Reuel Tolkien") +    assert tolkien.name == "John Ronald Reuel Tolkien" +    assert tolkien.id == tolkien_id + +    # alternatively update data without loading +    await Author.objects.filter(name__contains="Tolkien").update(name="J.R.R. Tolkien") + +    # to read more about updating data in the database +    # visit: https://collerek.github.io/ormar/queries/update/ + + +async def delete(): +    silmarillion = await Book.objects.get(year=1977) +    # call delete() on instance +    await silmarillion.delete() + +    # alternatively delete without loading +    await Book.objects.delete(title="The Tower of Fools") + +    # note that when there is no record ormar raises NoMatch exception +    try: +        await Book.objects.get(year=1977) +    except ormar.NoMatch: +        print("No book from 1977!") + +    # to read more about deleting data from the database +    # visit: https://collerek.github.io/ormar/queries/delete/ + +    # note that despite the fact that record no longer exists in database +    # the object above is still accessible and you can use it (and i.e. save()) again. +    tolkien = silmarillion.author +    await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977) + + +async def joins(): +    # Tho join two models use select_related + +    # Django style +    book = await Book.objects.select_related("author").get(title="The Hobbit") +    # Python style +    book = await Book.objects.select_related(Book.author).get( +        Book.title == "The Hobbit" +    ) + +    # now the author is already prefetched +    assert book.author.name == "J.R.R. Tolkien" + +    # By default you also get a second side of the relation +    # constructed as lowercase source model name +'s' (books in this case) +    # you can also provide custom name with parameter related_name + +    # Django style +    author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien") +    # Python style +    author = await Author.objects.select_related(Author.books).all( +        Author.name == "J.R.R. Tolkien" +    ) +    assert len(author[0].books) == 3 + +    # for reverse and many to many relations you can also prefetch_related +    # that executes a separate query for each of related models + +    # Django style +    author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien") +    # Python style +    author = await Author.objects.prefetch_related(Author.books).get( +        Author.name == "J.R.R. Tolkien" +    ) +    assert len(author.books) == 3 + +    # to read more about relations +    # visit: https://collerek.github.io/ormar/relations/ + +    # to read more about joins and subqueries +    # visit: https://collerek.github.io/ormar/queries/joins-and-subqueries/ + + +async def filter_and_sort(): +    # to filter the query you can use filter() or pass key-value pars to +    # get(), all() etc. +    # to use special methods or access related model fields use double +    # underscore like to filter by the name of the author use author__name +    # Django style +    books = await Book.objects.all(author__name="J.R.R. Tolkien") +    # python style +    books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien") +    assert len(books) == 3 + +    # filter can accept special methods also separated with double underscore +    # to issue sql query ` where authors.name like "%tolkien%"` that is not +    # case sensitive (hence small t in Tolkien) +    # Django style +    books = await Book.objects.filter(author__name__icontains="tolkien").all() +    # python style +    books = await Book.objects.filter(Book.author.name.icontains("tolkien")).all() +    assert len(books) == 3 + +    # to sort use order_by() function of queryset +    # to sort decreasing use hyphen before the field name +    # same as with filter you can use double underscores to access related fields +    # Django style +    books = ( +        await Book.objects.filter(author__name__icontains="tolkien") +        .order_by("-year") +        .all() +    ) +    # python style +    books = ( +        await Book.objects.filter(Book.author.name.icontains("tolkien")) +        .order_by(Book.year.desc()) +        .all() +    ) +    assert len(books) == 3 +    assert books[0].title == "The Silmarillion" +    assert books[2].title == "The Hobbit" + +    # to read more about filtering and ordering +    # visit: https://collerek.github.io/ormar/queries/filter-and-sort/ + + +async def subset_of_columns(): +    # to exclude some columns from loading when querying the database +    # you can use fileds() method +    hobbit = await Book.objects.fields(["title"]).get(title="The Hobbit") +    # note that fields not included in fields are empty (set to None) +    assert hobbit.year is None +    assert hobbit.author is None + +    # selected field is there +    assert hobbit.title == "The Hobbit" + +    # alternatively you can provide columns you want to exclude +    hobbit = await Book.objects.exclude_fields(["year"]).get(title="The Hobbit") +    # year is still not set +    assert hobbit.year is None +    # but author is back +    assert hobbit.author is not None + +    # also you cannot exclude primary key column - it's always there +    # even if you EXPLICITLY exclude it it will be there + +    # note that each model have a shortcut for primary_key column which is pk +    # and you can filter/access/set the values by this alias like below +    assert hobbit.pk is not None + +    # note that you cannot exclude fields that are not nullable +    # (required) in model definition +    try: +        await Book.objects.exclude_fields(["title"]).get(title="The Hobbit") +    except pydantic.ValidationError: +        print("Cannot exclude non nullable field title") + +    # to read more about selecting subset of columns +    # visit: https://collerek.github.io/ormar/queries/select-columns/ + + +async def pagination(): +    # to limit number of returned rows use limit() +    books = await Book.objects.limit(1).all() +    assert len(books) == 1 +    assert books[0].title == "The Hobbit" + +    # to offset number of returned rows use offset() +    books = await Book.objects.limit(1).offset(1).all() +    assert len(books) == 1 +    assert books[0].title == "The Lord of the Rings" + +    # alternatively use paginate that combines both +    books = await Book.objects.paginate(page=2, page_size=2).all() +    assert len(books) == 2 +    # note that we removed one book of Sapkowski in delete() +    # and recreated The Silmarillion - by default when no order_by is set +    # ordering sorts by primary_key column +    assert books[0].title == "The Witcher" +    assert books[1].title == "The Silmarillion" + +    # to read more about pagination and number of rows +    # visit: https://collerek.github.io/ormar/queries/pagination-and-rows-number/ + + +async def aggregations(): +    # count: +    assert 2 == await Author.objects.count() + +    # exists +    assert await Book.objects.filter(title="The Hobbit").exists() + +    # maximum +    assert 1990 == await Book.objects.max(columns=["year"]) + +    # minimum +    assert 1937 == await Book.objects.min(columns=["year"]) + +    # average +    assert 1964.75 == await Book.objects.avg(columns=["year"]) + +    # sum +    assert 7859 == await Book.objects.sum(columns=["year"]) + +    # to read more about aggregated functions +    # visit: https://collerek.github.io/ormar/queries/aggregations/ + + +async def raw_data(): +    # extract raw data in a form of dicts or tuples +    # note that this skips the validation(!) as models are +    # not created from parsed data + +    # get list of objects as dicts +    assert await Book.objects.values() == [ +        {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}, +        {"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955}, +        {"id": 4, "author": 2, "title": "The Witcher", "year": 1990}, +        {"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977}, +    ] + +    # get list of objects as tuples +    assert await Book.objects.values_list() == [ +        (1, 1, "The Hobbit", 1937), +        (2, 1, "The Lord of the Rings", 1955), +        (4, 2, "The Witcher", 1990), +        (5, 1, "The Silmarillion", 1977), +    ] + +    # filter data - note how you always get a list +    assert await Book.objects.filter(title="The Hobbit").values() == [ +        {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937} +    ] + +    # select only wanted fields +    assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [ +        {"id": 1, "title": "The Hobbit"} +    ] + +    # if you select only one column you could flatten it with values_list +    assert await Book.objects.values_list("title", flatten=True) == [ +        "The Hobbit", +        "The Lord of the Rings", +        "The Witcher", +        "The Silmarillion", +    ] + +    # to read more about extracting raw values +    # visit: https://collerek.github.io/ormar/queries/aggregations/ + + +async def with_connect(function): +    # note that for any other backend than sqlite you actually need to +    # connect to the database to perform db operations +    async with database: +        await function() + +    # note that if you use framework like `fastapi` you shouldn't connect +    # in your endpoints but have a global connection pool +    # check https://collerek.github.io/ormar/fastapi/ and section with db connection + + +# gather and execute all functions +# note - normally import should be at the beginning of the file +import asyncio + +# note that normally you use gather() function to run several functions +# concurrently but we actually modify the data and we rely on the order of functions +for func in [ +    create, +    read, +    update, +    delete, +    joins, +    filter_and_sort, +    subset_of_columns, +    pagination, +    aggregations, +    raw_data, +]: +    print(f"Executing: {func.__name__}") +    asyncio.run(with_connect(func)) + +# drop the database tables +metadata.drop_all(engine) +``` + +## Ormar Specification + +### QuerySet methods + +*  `create(**kwargs): -> Model` +*  `get(*args, **kwargs): -> Model` +*  `get_or_none(*args, **kwargs): -> Optional[Model]` +*  `get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]` +*  `first(*args, **kwargs): -> Model` +*  `update(each: bool = False, **kwargs) -> int` +*  `update_or_create(**kwargs) -> Model` +*  `bulk_create(objects: List[Model]) -> None` +*  `bulk_update(objects: List[Model], columns: List[str] = None) -> None` +*  `delete(*args, each: bool = False, **kwargs) -> int` +*  `all(*args, **kwargs) -> List[Optional[Model]]` +*  `iterate(*args, **kwargs) -> AsyncGenerator[Model]` +*  `filter(*args, **kwargs) -> QuerySet` +*  `exclude(*args, **kwargs) -> QuerySet` +*  `select_related(related: Union[List, str]) -> QuerySet` +*  `prefetch_related(related: Union[List, str]) -> QuerySet` +*  `limit(limit_count: int) -> QuerySet` +*  `offset(offset: int) -> QuerySet` +*  `count(distinct: bool = True) -> int` +*  `exists() -> bool` +*  `max(columns: List[str]) -> Any` +*  `min(columns: List[str]) -> Any` +*  `avg(columns: List[str]) -> Any` +*  `sum(columns: List[str]) -> Any` +*  `fields(columns: Union[List, str, set, dict]) -> QuerySet` +*  `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` +*  `order_by(columns:Union[List, str]) -> QuerySet` +*  `values(fields: Union[List, str, Set, Dict])` +*  `values_list(fields: Union[List, str, Set, Dict])` + + +#### Relation types + +*  One to many  - with `ForeignKey(to: Model)` +*  Many to many - with `ManyToMany(to: Model, Optional[through]: Model)` + +#### Model fields types + +Available Model Fields (with required args - optional ones in docs): + +* `String(max_length)` +* `Text()` +* `Boolean()` +* `Integer()` +* `Float()` +* `Date()` +* `Time()` +* `DateTime()` +* `JSON()` +* `BigInteger()` +* `SmallInteger()` +* `Decimal(scale, precision)` +* `UUID()` +* `LargeBinary(max_length)` +* `Enum(enum_class)` +* `Enum` like Field - by passing `choices` to any other Field type +* `EncryptedString` - by passing `encrypt_secret` and `encrypt_backend` +* `ForeignKey(to)` +* `ManyToMany(to)` + +### Available fields options +The following keyword arguments are supported on all field types. + +* `primary_key: bool` +* `nullable: bool` +* `default: Any` +* `server_default: Any` +* `index: bool` +* `unique: bool` +* `choices: typing.Sequence` +* `name: str` +* `pydantic_only: bool` + +All fields are required unless one of the following is set: + +* `nullable` - Creates a nullable column. Sets the default to `False`. Read the fields common parameters for details. +* `sql_nullable` - Used to set different setting for pydantic and the database. Sets the default to `nullable` value. Read the fields common parameters for details. +* `default` - Set a default value for the field. **Not available for relation fields** +* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields** +* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column. +  Autoincrement is set by default on int primary keys. +* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database. + +### Available signals + +Signals allow to trigger your function for a given event on a given Model. + +* `pre_save` +* `post_save` +* `pre_update` +* `post_update` +* `pre_delete` +* `post_delete` +* `pre_relation_add` +* `post_relation_add` +* `pre_relation_remove` +* `post_relation_remove` +* `post_bulk_update` + + +[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ +[databases]: https://github.com/encode/databases +[pydantic]: https://pydantic-docs.helpmanual.io/ +[encode/orm]: https://github.com/encode/orm/ +[alembic]: https://alembic.sqlalchemy.org/en/latest/ +[fastapi]: https://fastapi.tiangolo.com/ +[documentation]: https://collerek.github.io/ormar/ +[migrations]: https://collerek.github.io/ormar/models/migrations/ +[asyncio]: https://docs.python.org/3/library/asyncio.html +[releases]: https://collerek.github.io/ormar/releases/ +[tests]: https://github.com/collerek/ormar/tree/master/tests + + +%prep +%autosetup -n ormar-0.12.1 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then +	find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then +	find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then +	find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then +	find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then +	find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-ormar -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Wed Apr 12 2023 Python_Bot <Python_Bot@openeuler.org> - 0.12.1-1 +- Package Spec generated @@ -0,0 +1 @@ +039b5793e2d4f4ff76928696a7281289  ormar-0.12.1.tar.gz | 
