{"id":49513094,"url":"https://github.com/manoss96/onlymaps","last_synced_at":"2026-05-01T21:02:07.601Z","repository":{"id":325745550,"uuid":"1101901665","full_name":"manoss96/onlymaps","owner":"manoss96","description":"A Python micro-ORM","archived":false,"fork":false,"pushed_at":"2026-01-11T14:32:07.000Z","size":567,"stargazers_count":327,"open_issues_count":0,"forks_count":9,"subscribers_count":5,"default_branch":"main","last_synced_at":"2026-04-27T12:05:56.248Z","etag":null,"topics":["database","mariadb","mysql","orm","postgresql","python","sql","sqlite","sqlserver"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/onlymaps/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/manoss96.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["manoss96"]}},"created_at":"2025-11-22T13:01:40.000Z","updated_at":"2026-04-24T09:02:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/manoss96/onlymaps","commit_stats":null,"previous_names":["manoss96/onlymaps"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/manoss96/onlymaps","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manoss96%2Fonlymaps","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manoss96%2Fonlymaps/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manoss96%2Fonlymaps/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manoss96%2Fonlymaps/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/manoss96","download_url":"https://codeload.github.com/manoss96/onlymaps/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manoss96%2Fonlymaps/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32512670,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-30T13:12:12.517Z","status":"online","status_checked_at":"2026-05-01T02:00:05.856Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","mariadb","mysql","orm","postgresql","python","sql","sqlite","sqlserver"],"created_at":"2026-05-01T21:02:06.907Z","updated_at":"2026-05-01T21:02:07.588Z","avatar_url":"https://github.com/manoss96.png","language":"Python","funding_links":["https://github.com/sponsors/manoss96"],"categories":[],"sub_categories":[],"readme":"\u003c!-- PROJECT BADGES --\u003e\n[![Python Version][python-shield]][python-url]\n[![MIT License][license-shield]][license-url]\n[![Coverage][coverage-shield]][coverage-url]\n\n![OnlyMaps Logo](docs/source/onlymaps.png)\n\n\nOnlymaps is a Python micro-ORM library that lets you interact with a database\nthrough plain SQL while it takes care of mapping any query results back to Python\nobjects. More specifically, it provides:\n\n- A minimal API that enables both sync and async query execution.\n- Fine-grained type-hinting and validation with the help of [Pydantic](https://docs.pydantic.dev/latest/).\n- Support for all major databases such as PostgreSQL, MySQL, MariaDB, MS SQL Server and more.\n- Connection pooling via custom implementation.\n\n## How to install 📦\n\nYou can install Onlymaps by running `pip install onlymaps`. If your virtual environment is missing any required database driver\npackages, an exception with an appropriate message will be raised when trying to establish a connection for the first time, \nfor example:\n\n```\nImportError: Package `psycopg` not found. Please run `pip install onlymaps[psycopg]`.\n```\n\n## Documentation 📖\n\n- [Connecting to a database](#connecting-to-a-database)\n    + [Establishing a connection](#establishing-a-connection)\n    + [Building the connection string](#building-the-connection-string)\n    + [Using unsupported drivers](#using-unsupported-drivers)\n    + [Enabling connection pooling](#enabling-connection-pooling)\n- [Running queries](#running-queries)\n    + [Query execution](#query-execution)\n    + [Passing arguments to queries](#passing-arguments-to-queries)\n    + [Query parameter wrappers](#query-parameter-wrappers)\n    + [Managing transactions](#managing-transactions)\n- [Mapping query results](#mapping-query-results)\n    - [Single-column queries](#single-column-queries)\n    - [Multi-column queries](#multi-column-queries)\n\n### Connecting to a database\n\nThis chapter explains how to access the `Database` and `AsyncDatabase` APIs.\n\n#### Establishing a connection\n\nOnlymaps exposes a single entrypoint to its sync API, i.e. the function `onlymaps.connect`:\n\n```python\nfrom onlymaps import connect\n\nwith connect(\"postgresql://user:password@localhost:5432/mydb\") as db:\n    # Execute queries...\n```\n\nSimilarly, `onlymaps.asyncio.connect` gives access to its async counterpart, namely `AsyncDatabase`:\n\n```python\nfrom onlymaps.asyncio import connect\n\nasync with connect(\"postgresql://user:password@localhost:5432/mydb\") as db:\n    # Execute queries...\n```\n\nUsing a `Database` object in a `with` statement manages opening and closing the underlying connection for you. However, you can always handle this yourself if you choose to do so:\n\n```python\nfrom onlymaps import connect, Database\n\ndb: Database = connect(\"postgresql://user:password@localhost:5432/mydb\")\n\ndb.open()\n# Execute queries...\ndb.close()\n```\n\nSince the sync and async APIs are identical, from now on we will be using the sync API for all examples. You only have to remember that when using the async API, methods must be awaited:\n\n```python\nfrom onlymaps.asyncio import connect, AsyncDatabase\n\ndb: AsyncDatabase = connect(\"postgresql://user:password@localhost:5432/mydb\")\n\nawait db.open()\n# Execute queries...\nawait db.close()\n```\n\n#### Building the connection string\n\nIn order to connect to a database you must provide the `connect` function with a valid connection string, i.e. a string that has the following format:\n\n```\n{DB_TYPE}://{USERNAME}[:{PASSWORD}]@{HOST}:{PORT}/{DB_NAME}\n```\n\nThe `{DB_TYPE}` placeholder can take any of the following values depending on the type of database you are trying to connect to:\n\n- `postgresql`: PostgreSQL\n- `mysql`: MySQL\n- `mssql`: Microsoft SQL Server\n- `mariadb`: MariaDB\n- `oraceldb`: Oracle Database\n- `sqlite`: SQLite. More specifically, when connecting to a SQLite database, your connection string must be formatted as such: `sqlite:///{DB_NAME}`.\n- `duckdb`: DuckDB. Similarly, when connecting to a DuckDB database, your connection string must be formatted as such: `duckdb:///{DB_NAME}`.\n\n#### Using unsupported drivers\n\nBesides using a connection string, there is a second way of connecting to a database,\nwhich also enables you to use a driver of your choice even if it is not \"officially\"\nsupported by Onlymaps. The way to do this is by providing `onlymaps.connect` with a\nconnection factory, i.e. a parameterless function that, when invoked, outputs a\ndatabase connection instance:\n\n```python\nfrom functools import partial\n\nfrom onlymaps import connect\nfrom pydbapiv2_compatible_driver import connect as pydbapiv2_connect\n\nconn_factory = partial(\n    pydbapiv2_connect,\n    \"\u003cMY_CONN_STRING\u003e\",\n    conn_arg_1=...,\n    conn_arg_2=...,\n)\n\ndb = connect(conn_factory)\n```\n\nYou only have to remember that, for this to work, the factory function must output connection\ninstances that implement the `Connection` interface of [Python Database API Specification v2.0](https://peps.python.org/pep-0249/).\nTherefore, as long as a Python database driver package is compatible with the afformentioned protocol,\nyou can use it and it will work just fine!\n\n#### Enabling connection pooling\n\nConnection pooling can be activated by setting the `pooling` parameter to `True`:\n\n```python\nfrom onlymaps import connect\n\nwith connect(\n    \"postgresql://user:password@localhost:5432/mydb\",\n    pooling=True,\n    max_pool_size=100\n) as db:\n    # Object `db` now uses a connection pool for query execution underneath.\n```\n\nWhile both a connection and a connection pool are thread-safe, the latter is\nmuch more suitable for multithreaded applications as a single connection is\nrestricted from executing more than one query at a time. This means that if\ntwo or more threads are sharing the same connection, they will be competing\nwith each other for query execution time. This is not an issue when using\na connection pool with an adequate number of connections, where each thread\ncan get its own connection from the pool.\n\nThe same is true for the async variants as well. Both the async connection and the\nasync connection pool can be considered concurrency-safe, as long as they are not accessed\noutside the event loop in which they were created. When it comes to their performance,\nconnection pooling is again the right choice for a heavily concurrent application,\nfor example an ASGI web server.\n\n\n### Running queries\n\nThis chapter focuses on querying the database.\n\n#### Query execution\n\nThe `Database` API includes five different methods for query execution:\n\n1. `exec`: Executes a query and returns `None`.\n2. `fetch_one_or_none`: Executes a query and returns either a single row or `None`.\n3. `fetch_one`: Executes a query and returns a single row.\n4. `fetch_many`: Executes a query and returns a list of rows.\n5. `iter`: Executes a query and returns an iterator over rows.\n\nConsider for example the following code:\n\n```python\nfrom typing import Any\n\nusers: list[Any] = db.fetch_many(..., \"SELECT name, age FROM users\")\n```\n\nThe ellipsis `...` is used when you don't care about the type and just want to retrieve some rows.\nHowever, in most cases you'd probably like to enforce a type on the result:\n\n```python\nfrom pydantic import BaseModel\n\nclass User(BaseModel):\n    name: str\n    age: int\n\nusers: list[User] = db.fetch_many(User, \"SELECT name, age FROM users\")\n```\n\nEven though the above example uses a Pydantic model, you are not required to use one.\nIn fact, you can use any type you like as long as it matches the result you are expecting:\n\n```python\nusers: list[tuple[str, int]] = db.fetch_many(tuple[str, int], \"SELECT name, age FROM users\")\n```\n\nKeep in mind, however, that `fetch_many` goes on to retrieve all possible rows from the database.\nIf that's something you don't want, then you should use a `LIMIT` clause in your SQL query,\notherwise you might run into memory issues if your database table is very large. If you must query\na very large table, method `iter` comes in handy as it enables you to fetch rows from the database\nin distinct batches over many iterations:\n\n```python\nwith db.iter(\n    tuple[str, int],\n    100, # This is the size of each batch.\n    \"SELECT name, age FROM users\"\n) as it:\n    for batch in it:\n        # Each batch contains data for 100 users.\n```\n\n#### Passing arguments to queries\n\nAny argument that comes after the query, is meant to be used as a query parameter:\n\n```python\nrow = db.fetch_one(tuple[int, str], \"SELECT %s, %s\", 1, \"Hi!\")\n\nprint(row) # Prints `(1, 'Hi!')`.\n```\n\nThis is true for keyword arguments as well:\n\n```python\nrow = db.fetch_one(tuple[int, str], \"SELECT %(_id)s, %(msg)s\", _id=1, msg=\"Hi!\")\n\nprint(row) # Prints `(1, 'Hi!')`.\n```\n\nThough mixing positional and keyword arguments together is not allowed:\n```python\n# This raises a `ValueError` exception.\nrow_2 = db.fetch_one(tuple[int, str], \"SELECT %s, %(msg)s\", 1, msg=\"Hi!\")\n```\n\nNote that in the above examples we use symbol `%s` for positional parameters and `%(\u003cVAR\u003e)s` for keyword parameters.\nHowever, the exact parameter symbol you must use depends on the underlying database driver. For example,\nthe SQLite driver uses symbols `?` and `:\u003cVAR\u003e` respectively. In order to determine what parameter symbol\nyou should use, you can take a look at the documentation of the driver you are using.\n\n#### Query parameter wrappers\n\nOnlymaps does some parameter handling by default when it makes sense to do so. To give an example, `uuid.UUID` and `pydantic.BaseModel` type instances are always converted into strings when provided as query parameters, as this is the only type conversion that makes sense in this context. However, there exist certain cases where it is not so obvious how a parameter is meant to be used. Consider the following example:\n\n```python\nids = [1, 2, 3, 4, 5]\n\nrows = db.fetch_many(..., \"SELECT * FROM my_table WHERE id = ANY(%s)\", ids)\n```\n\nThe `psycopg` driver is able to handle `list` type arguments just fine, and goes on to fetch\nall rows in `my_table` whose id can be found within the list. But a list query parameter could\nalso be used in other ways, for example, being converted into a JSON string and inserted into the\ndatabase.\n\nFor this reason exists the `Json` parameter wrapper class, which indicates that\nan argument is meant to be converted into a JSON string before being passed on to the driver, as\nlong as that's feasible:\n\n```python\nfrom onlymaps import Json\n\nids = [1, 2, 3, 4, 5]\nkv_pairs = {\"a\": 1, \"b\": 2}\n\ndb.exec(\"INSERT INTO my_table (col_a, col_b) VALUES (%s, %s)\", Json(ids), Json(kv_pairs))\n```\n\nBoth `ids` and `kv_pairs` are converted into JSON-compatible strings, which are then inserted\ninto the table columns `col_a` and `col_b` respectively.\n\nOther than `Json`, there exists one more parameter wrapper class, namely `Bulk`, which in turn indicates\nthat the provided argument is to be executed as part of a bulk statement:\n\n```python\nfrom onlymaps import Bulk\n\nrows = [[1, \"a\"], [2, \"b\"], [3, \"c\"]]\n\ndb.exec(\"INSERT INTO my_table (id, label) VALUES (%s, %s)\", Bulk(rows))\n```\n\nAs for how the bulk statement is actually being executed, that depends on the underlying driver being used.\n\n#### Managing transactions\n\nOnlymaps completely abstracts away the concepts of commit and rollback for the sake of simplicity. This means that if a query execution method call exits successfully, you should consider any changes done by the query committed to the database. On the other hand, if a method call raises an exception, even if said exception is raised during the result mapping stage, no changes are committed.\n\nNevertheless, sometimes you need to execute a bunch of queries together so that either all succeed or none does. In that case, the `Database` API provides a `transaction` context manager method:\n\n```python\nwith db.transaction():\n    db.exec(INSERT_ROW_INTO_TABLE_A_QUERY)\n    db.exec(INSERT_ROW_INTO_TABLE_B_QUERY)\n    db.exec(DELETE_ROW_FROM_TABLE_C_QUERY)\n```\n\nIf an exception is raised before the transaction exits, any changes\ndone by any of the queries are discarded. Else, if the transaction\nexits successfully, all changes are persisted.\n\n\n### Mapping query results\n\nIn order to better understand how rows are mapped to Python objects, it is crucial\nthat we make the distinction between querying a single column and querying multiple columns.\n\n##### Single-column queries\n\nWhen querying a single column, the type you use must match the type of the column you are selecting.\nIf, for example, your query selects an integer id column, then you should use `int` as a type:\n\n```python\nids: list[int] = db.fetch_many(int, \"SELECT id FROM my_table\")\n```\n\nThe type of the column can be as simple as an integer, or as complex as a JSON\nstring with a predefined schema:\n\n```python\nfrom pydantic import BaseModel\n\nclass JsonColumn(BaseModel):\n    id: int\n    label: str\n    metadata: str | None = None\n\njson_cols: list[JsonColumn] = db.fetch_many(JsonColumn, \"SELECT json_col FROM my_table\")\n```\n\nIn general, when querying a single column, the following types are supported:\n\n- `bool`\n- `int`\n- `float`\n- `decimal.Decimal`\n- `str`\n- `bytes`\n- `uuid.UUID`\n- `datetime.date`\n- `datetime.datetime`\n- `Enum`\n- `tuple`\n- `list`\n- `set`\n- `dict`\n- `dataclasses.dataclass`\n- `pydantic.dataclasses.dataclass`\n- `pydantic.BaseModel`\n\n##### Multi-column queries\n\n\nThings are a bit different when querying multiple columns, as the type you must use\nshould always be some sorts of struct type which is able to contain more than one\ntype of data:\n\n```python\nrows: list[tuple] = db.fetch_many(tuple, \"SELECT id, label FROM my_table\")\n```\n\nThe complete list of types supported when querying multiple columns is as follows:\n\n- `tuple`\n- `list`\n- `set`\n- `dict`\n- `dataclasses.dataclass`\n- `pydantic.dataclasses.dataclass`\n- `pydantic.BaseModel`\n\nThis  above list of struct types can be further separated into two distinct categories:\n\n- Container types: `tuple`, `list`, `set`.\n- Model types: `dict`, `dataclasses.dataclass`, `pydantic.dataclasses.dataclass`, `pydantic.BaseModel`\n\nWhen using a container type, you lose all information regarding the column names.\nIf you wish to retain that, you should use a model type:\n\n```python\nrows: list[dict] = db.fetch_many(dict, \"SELECT id, label FROM my_table\")\n\nprint(rows[0].keys()) # This prints `dict_keys(['id', 'label'])`.\n```\n\nYou can use whichever suits you best, as both container types and\nmodel types can be parametrized in order to further validate the\ntype of values you are expecting:\n\n```python\n\n# This works!\nrows1 = db.fetch_many(tuple[int, str], \"SELECT id, label FROM my_table\")\n\n# And this works!\nrows2 = db.fetch_many(dict[str, int | str], \"SELECT id, label FROM my_table\")\n\n# But these two raise a `TypeError`.\nrows3 = db.fetch_many(tuple[int, int], \"SELECT id, label FROM my_table\")\nrows4 = db.fetch_many(dict[str, int], \"SELECT id, label FROM my_table\")\n```\n\n\n\u003c!-- MARKDOWN LINKS \u0026 IMAGES --\u003e\n[python-shield]: https://img.shields.io/badge/python-3.11+-blue\n[python-url]: https://www.python.org/downloads/release/python-390/\n[license-shield]: https://img.shields.io/badge/license-MIT-red\n[license-url]: https://github.com/manoss96/onlymaps/blob/main/LICENSE.txt\n[coverage-shield]: https://coveralls.io/repos/github/manoss96/onlymaps/badge.svg?branch=main\u0026service=github\n[coverage-url]: https://coveralls.io/github/manoss96/onlymaps?branch=main\n[docs-url]: https://github.com/manoss96/onlymaps?tab=readme-ov-file#documentation-\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmanoss96%2Fonlymaps","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmanoss96%2Fonlymaps","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmanoss96%2Fonlymaps/lists"}