encino-orm 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- encino_orm/__init__.py +62 -0
- encino_orm/base.py +162 -0
- encino_orm/cli.py +139 -0
- encino_orm/context.py +61 -0
- encino_orm/engine.py +45 -0
- encino_orm/exceptions.py +22 -0
- encino_orm/graphql/__init__.py +9 -0
- encino_orm/graphql/filters.py +211 -0
- encino_orm/graphql/resolvers.py +65 -0
- encino_orm/graphql/scalars.py +19 -0
- encino_orm/graphql/schema.py +190 -0
- encino_orm/graphql/types.py +72 -0
- encino_orm/http/__init__.py +45 -0
- encino_orm/http/errors.py +22 -0
- encino_orm/http/parsing.py +65 -0
- encino_orm/http/registry.py +32 -0
- encino_orm/http/routes.py +108 -0
- encino_orm/introspection/__init__.py +13 -0
- encino_orm/introspection/codegen.py +106 -0
- encino_orm/introspection/tables.py +11 -0
- encino_orm/introspection/types.py +135 -0
- encino_orm/migration.py +56 -0
- encino_orm/model/__init__.py +111 -0
- encino_orm/model/cache_backend.py +59 -0
- encino_orm/model/cached.py +47 -0
- encino_orm/model/column.py +7 -0
- encino_orm/model/constraint.py +119 -0
- encino_orm/model/domain.py +65 -0
- encino_orm/model/exceptions.py +33 -0
- encino_orm/model/filter.py +200 -0
- encino_orm/model/hooks.py +34 -0
- encino_orm/model/index.py +27 -0
- encino_orm/model/model.py +1009 -0
- encino_orm/model/query_builder.py +286 -0
- encino_orm/model/records.py +22 -0
- encino_orm/model/references.py +32 -0
- encino_orm/model/scope.py +26 -0
- encino_orm/model/types.py +195 -0
- encino_orm/mysql.py +270 -0
- encino_orm/observability.py +149 -0
- encino_orm/pool.py +301 -0
- encino_orm/postgresql.py +258 -0
- encino_orm/query.py +34 -0
- encino_orm/security/__init__.py +33 -0
- encino_orm/security/exceptions.py +20 -0
- encino_orm/security/guard.py +71 -0
- encino_orm/security/jwt.py +56 -0
- encino_orm/security/models.py +62 -0
- encino_orm/security/permissions.py +56 -0
- encino_orm/sql.py +142 -0
- encino_orm/sqlite.py +245 -0
- encino_orm/transfer.py +177 -0
- encino_orm-0.2.0.dist-info/METADATA +168 -0
- encino_orm-0.2.0.dist-info/RECORD +57 -0
- encino_orm-0.2.0.dist-info/WHEEL +4 -0
- encino_orm-0.2.0.dist-info/entry_points.txt +2 -0
- encino_orm-0.2.0.dist-info/licenses/LICENSE +21 -0
encino_orm/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from .base import Db
|
|
2
|
+
from .context import bind, get_default_db, resolve_db, set_default_db
|
|
3
|
+
from .engine import Engine, engine_of, is_mysql, is_postgres, is_sqlite
|
|
4
|
+
from .query import Query
|
|
5
|
+
from .sql import SqlFunctions, Weekday
|
|
6
|
+
from .sqlite import SqliteDb
|
|
7
|
+
from .mysql import MysqlDb
|
|
8
|
+
from .postgresql import PostgresDb
|
|
9
|
+
from .pool import PoolDb, create_db, session
|
|
10
|
+
from .observability import OtelQueryTracer, QueryTracer, current_trace_id, trace_id
|
|
11
|
+
from .migration import (
|
|
12
|
+
Migration,
|
|
13
|
+
apply_migration,
|
|
14
|
+
apply_migrations,
|
|
15
|
+
migrations_from_dir,
|
|
16
|
+
rollback_migration,
|
|
17
|
+
)
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
EncinoOrmError,
|
|
20
|
+
ConnectionError,
|
|
21
|
+
QueryError,
|
|
22
|
+
UnsupportedEngineError,
|
|
23
|
+
MigrationError,
|
|
24
|
+
PoolExhaustedError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Db",
|
|
29
|
+
"Query",
|
|
30
|
+
"Engine",
|
|
31
|
+
"engine_of",
|
|
32
|
+
"is_sqlite",
|
|
33
|
+
"is_mysql",
|
|
34
|
+
"is_postgres",
|
|
35
|
+
"SqlFunctions",
|
|
36
|
+
"Weekday",
|
|
37
|
+
"SqliteDb",
|
|
38
|
+
"MysqlDb",
|
|
39
|
+
"PostgresDb",
|
|
40
|
+
"PoolDb",
|
|
41
|
+
"create_db",
|
|
42
|
+
"session",
|
|
43
|
+
"set_default_db",
|
|
44
|
+
"get_default_db",
|
|
45
|
+
"bind",
|
|
46
|
+
"resolve_db",
|
|
47
|
+
"QueryTracer",
|
|
48
|
+
"OtelQueryTracer",
|
|
49
|
+
"trace_id",
|
|
50
|
+
"current_trace_id",
|
|
51
|
+
"Migration",
|
|
52
|
+
"apply_migration",
|
|
53
|
+
"apply_migrations",
|
|
54
|
+
"migrations_from_dir",
|
|
55
|
+
"rollback_migration",
|
|
56
|
+
"EncinoOrmError",
|
|
57
|
+
"ConnectionError",
|
|
58
|
+
"QueryError",
|
|
59
|
+
"UnsupportedEngineError",
|
|
60
|
+
"MigrationError",
|
|
61
|
+
"PoolExhaustedError",
|
|
62
|
+
]
|
encino_orm/base.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import random
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
|
|
7
|
+
from .query import Query
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("encino_orm")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Db(ABC):
|
|
13
|
+
MAX_TRIES = 9
|
|
14
|
+
WAITERS = [x * 0.02 for x in range(1, 11)]
|
|
15
|
+
MAX_WAIT = len(WAITERS) - 1
|
|
16
|
+
|
|
17
|
+
dialect: str = ""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def fn(self):
|
|
21
|
+
"""Namespace de funciones SQL portables (`db.fn.now()`, `db.fn.date_add(...)`)."""
|
|
22
|
+
from .sql import SqlFunctions
|
|
23
|
+
|
|
24
|
+
return SqlFunctions(self.dialect)
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def connect(self, **kwargs): ...
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def close(self): ...
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def is_alive(self): ...
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
async def in_transaction(self): ...
|
|
37
|
+
|
|
38
|
+
@asynccontextmanager
|
|
39
|
+
async def transaction(self):
|
|
40
|
+
try:
|
|
41
|
+
yield
|
|
42
|
+
await self.commit()
|
|
43
|
+
except Exception:
|
|
44
|
+
await self.rollback()
|
|
45
|
+
raise
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def commit(self): ...
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
async def rollback(self, save_point: str = None): ...
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def save_point(self, name: str): ...
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
async def wait(waiter: int = -1):
|
|
58
|
+
"""Mecanismo de espera en caso de bloqueo por deadlock en la base de datos."""
|
|
59
|
+
if waiter < 0:
|
|
60
|
+
waiter = random.randint(0, Db.MAX_WAIT)
|
|
61
|
+
await asyncio.sleep(Db.WAITERS[waiter])
|
|
62
|
+
return waiter
|
|
63
|
+
|
|
64
|
+
def is_lock_error(self, exc: Exception) -> bool:
|
|
65
|
+
"""Indica si `exc` corresponde a un error de bloqueo/deadlock re-reintentable."""
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
async def retry(self, coro, tries: int = None):
|
|
69
|
+
"""Reintenta una coroutine ante errores de bloqueo (deadlock)."""
|
|
70
|
+
max_tries = tries if tries is not None else self.MAX_TRIES
|
|
71
|
+
last_exc = None
|
|
72
|
+
for attempt in range(max_tries):
|
|
73
|
+
try:
|
|
74
|
+
return await coro()
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
if not self.is_lock_error(exc):
|
|
77
|
+
raise
|
|
78
|
+
last_exc = exc
|
|
79
|
+
if attempt + 1 >= max_tries:
|
|
80
|
+
break
|
|
81
|
+
await self.wait()
|
|
82
|
+
raise last_exc
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def insert(self, tabla: str, data: dict, ignore_duplicated=False, replace=False): ...
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def delete(self, tabla: str, keys: dict): ...
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def update(self, tabla: str, keys: dict, values: dict): ...
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
async def execute(self, qry: Query): ...
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
async def fetch_all(self, qry: Query): ...
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
async def fetch_one(self, qry: Query): ...
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
async def fetch_many(self, qry: Query, limit: int, page: int): ...
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
async def exists(self, qry: Query): ...
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
async def last_id(self): ...
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
async def migrate(self, name: str, qry: Query): ...
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
async def migrate_status(self): ...
|
|
116
|
+
|
|
117
|
+
def _tables_sql(self) -> str:
|
|
118
|
+
"""SQL base que lista las tablas del catálogo (por motor).
|
|
119
|
+
|
|
120
|
+
Opcional: los motores que no lo implementen no soportan `list_tables`.
|
|
121
|
+
"""
|
|
122
|
+
raise NotImplementedError("introspección no soportada para este motor")
|
|
123
|
+
|
|
124
|
+
async def columns_of(self, table: str) -> list:
|
|
125
|
+
"""Devuelve la especificación de columnas de una tabla (por motor).
|
|
126
|
+
|
|
127
|
+
Opcional: los motores que no lo implementen no soportan `columns_of`.
|
|
128
|
+
"""
|
|
129
|
+
raise NotImplementedError("introspección no soportada para este motor")
|
|
130
|
+
|
|
131
|
+
async def list_tables(self, *, name: str = "", limit: int = 50, page: int = 1):
|
|
132
|
+
"""Lista las tablas del catálogo con filtro por nombre y paginación."""
|
|
133
|
+
from .model.records import Records
|
|
134
|
+
|
|
135
|
+
sql = self._tables_sql()
|
|
136
|
+
params = []
|
|
137
|
+
if name:
|
|
138
|
+
sql += " AND name LIKE {0}"
|
|
139
|
+
params.append(f"%{name}%")
|
|
140
|
+
total = (await self.fetch_one(Query(f"SELECT COUNT(*) FROM ({sql})", params)))["COUNT(*)"]
|
|
141
|
+
rows = await self.fetch_many(Query(sql, params), limit, page)
|
|
142
|
+
return Records(rows=rows, total=total, limit=limit, page=page)
|
|
143
|
+
|
|
144
|
+
async def paginate(self, qry: Query, limit: int, page: int = 1):
|
|
145
|
+
"""Devuelve un `Records` con la página y el total de un `Query` raw.
|
|
146
|
+
|
|
147
|
+
El total se calcula envolviendo el SQL en
|
|
148
|
+
``SELECT COUNT(*) AS n FROM (...)``, por lo que solo es fiable para
|
|
149
|
+
SELECT simples (sin ``;`` final, sin su propio ``LIMIT``/``OFFSET`` y sin
|
|
150
|
+
cláusulas no re-embebibles como ``FOR UPDATE``). Conviene incluir
|
|
151
|
+
``ORDER BY`` en el SQL para una paginación estable.
|
|
152
|
+
"""
|
|
153
|
+
from .model.records import Records
|
|
154
|
+
|
|
155
|
+
rows = await self.fetch_many(qry, limit, page)
|
|
156
|
+
sql = qry.sql_template.strip().rstrip(";")
|
|
157
|
+
count_qry = Query(
|
|
158
|
+
f"SELECT COUNT(*) AS n FROM ({sql}) _encino_orm_count", list(qry.fields)
|
|
159
|
+
)
|
|
160
|
+
row = await self.fetch_one(count_qry)
|
|
161
|
+
total = row["n"] if row else 0
|
|
162
|
+
return Records(rows=rows, total=total, limit=limit, page=page)
|
encino_orm/cli.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""CLI de encino_orm (solo stdlib `argparse`).
|
|
2
|
+
|
|
3
|
+
Subcomandos: `encino_orm generate models <engine> [tablas...]` y
|
|
4
|
+
`encino_orm copy <src-engine> <dst-engine> [tablas...]`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .engine import Engine
|
|
12
|
+
|
|
13
|
+
_ENGINE_CHOICES = [e.value for e in Engine]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _add_conn_args(parser, prefix: str, label: str):
|
|
17
|
+
def flag(name):
|
|
18
|
+
return f"--{prefix}-{name}" if prefix else f"--{name}"
|
|
19
|
+
|
|
20
|
+
parser.add_argument(flag("database"), help=f"archivo (sqlite) o nombre de BD {label}")
|
|
21
|
+
parser.add_argument(flag("host"), help=f"host {label} (mysql/postgresql)")
|
|
22
|
+
parser.add_argument(flag("port"), type=int, help=f"puerto {label} (mysql/postgresql)")
|
|
23
|
+
parser.add_argument(flag("user"), help=f"usuario {label} (mysql/postgresql)")
|
|
24
|
+
parser.add_argument(flag("password"), help=f"contraseña {label} (mysql/postgresql)")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
28
|
+
parser = argparse.ArgumentParser(
|
|
29
|
+
prog="encino_orm",
|
|
30
|
+
description="Herramientas de encino_orm (codegen y copia de datos).",
|
|
31
|
+
)
|
|
32
|
+
sub = parser.add_subparsers(dest="command")
|
|
33
|
+
|
|
34
|
+
gen = sub.add_parser("generate", help="genera código desde la base de datos")
|
|
35
|
+
gen_sub = gen.add_subparsers(dest="subcommand")
|
|
36
|
+
|
|
37
|
+
models = gen_sub.add_parser("models", help="genera modelos desde tablas existentes")
|
|
38
|
+
models.add_argument("engine", choices=_ENGINE_CHOICES)
|
|
39
|
+
models.add_argument("tables", nargs="*", help="tablas a generar (default: todas)")
|
|
40
|
+
models.add_argument("--folder", default="models", help="carpeta de salida (default: models)")
|
|
41
|
+
_add_conn_args(models, "", "")
|
|
42
|
+
|
|
43
|
+
copy = sub.add_parser("copy", help="copia tablas/BD completa entre bases de datos")
|
|
44
|
+
copy.add_argument("src_engine", choices=_ENGINE_CHOICES)
|
|
45
|
+
copy.add_argument("dst_engine", choices=_ENGINE_CHOICES)
|
|
46
|
+
copy.add_argument("tables", nargs="*", help="tablas a copiar (default: todas)")
|
|
47
|
+
copy.add_argument("--create", action="store_true", help="crea las tablas en destino")
|
|
48
|
+
copy.add_argument("--truncate", action="store_true", help="vacía cada tabla destino antes de copiar")
|
|
49
|
+
copy.add_argument("--no-preserve-ids", action="store_true",
|
|
50
|
+
help="no copiar la PK auto-incremental (dejar que el destino la asigne)")
|
|
51
|
+
copy.add_argument("--no-disable-fk", action="store_true",
|
|
52
|
+
help="no desactivar las restricciones de FK durante la copia")
|
|
53
|
+
_add_conn_args(copy, "src", "origen")
|
|
54
|
+
_add_conn_args(copy, "dst", "destino")
|
|
55
|
+
|
|
56
|
+
return parser
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _conn_kwargs(args) -> dict:
|
|
60
|
+
return _conn_kwargs_prefixed(args, "engine", "")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _conn_kwargs_prefixed(args, engine_attr: str, prefix: str) -> dict:
|
|
64
|
+
engine = getattr(args, engine_attr)
|
|
65
|
+
|
|
66
|
+
def get(name):
|
|
67
|
+
full = f"{prefix}_{name}" if prefix else name
|
|
68
|
+
return getattr(args, full, None)
|
|
69
|
+
|
|
70
|
+
if engine == Engine.SQLITE:
|
|
71
|
+
return {"database": get("database") or ":memory:"}
|
|
72
|
+
kw = {}
|
|
73
|
+
if get("host"):
|
|
74
|
+
kw["host"] = get("host")
|
|
75
|
+
if get("port"):
|
|
76
|
+
kw["port"] = get("port")
|
|
77
|
+
if get("user"):
|
|
78
|
+
kw["user"] = get("user")
|
|
79
|
+
if get("password"):
|
|
80
|
+
kw["password"] = get("password")
|
|
81
|
+
if get("database"):
|
|
82
|
+
kw["db" if engine == Engine.MYSQL else "database"] = get("database")
|
|
83
|
+
return kw
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def _generate_models(args) -> int:
|
|
87
|
+
from .introspection import generate_model, list_tables
|
|
88
|
+
from .pool import create_db
|
|
89
|
+
|
|
90
|
+
db = await create_db(args.engine, **_conn_kwargs(args))
|
|
91
|
+
try:
|
|
92
|
+
tables = args.tables or [r["name"] for r in (await list_tables(db)).rows]
|
|
93
|
+
for table in tables:
|
|
94
|
+
print(await generate_model(db, table, folder=args.folder))
|
|
95
|
+
return 0
|
|
96
|
+
finally:
|
|
97
|
+
await db.close()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def _copy(args) -> int:
|
|
101
|
+
from .pool import create_db
|
|
102
|
+
from .transfer import copy_database
|
|
103
|
+
|
|
104
|
+
src = await create_db(args.src_engine, **_conn_kwargs_prefixed(args, "src_engine", "src"))
|
|
105
|
+
try:
|
|
106
|
+
dst = await create_db(args.dst_engine, **_conn_kwargs_prefixed(args, "dst_engine", "dst"))
|
|
107
|
+
try:
|
|
108
|
+
result = await copy_database(
|
|
109
|
+
src, dst, tables=args.tables or None,
|
|
110
|
+
create=args.create, truncate=args.truncate,
|
|
111
|
+
preserve_ids=not args.no_preserve_ids,
|
|
112
|
+
disable_fk=not args.no_disable_fk,
|
|
113
|
+
)
|
|
114
|
+
for table, count in result.items():
|
|
115
|
+
print(f"{table}: {count} filas")
|
|
116
|
+
return 0
|
|
117
|
+
finally:
|
|
118
|
+
await dst.close()
|
|
119
|
+
finally:
|
|
120
|
+
await src.close()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main(argv=None) -> int:
|
|
124
|
+
parser = _build_parser()
|
|
125
|
+
args = parser.parse_args(argv)
|
|
126
|
+
if args.command == "generate" and args.subcommand == "models":
|
|
127
|
+
try:
|
|
128
|
+
return asyncio.run(_generate_models(args))
|
|
129
|
+
except Exception as exc: # pragma: no cover - depende del entorno
|
|
130
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
131
|
+
return 1
|
|
132
|
+
if args.command == "copy":
|
|
133
|
+
try:
|
|
134
|
+
return asyncio.run(_copy(args))
|
|
135
|
+
except Exception as exc: # pragma: no cover - depende del entorno
|
|
136
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
137
|
+
return 1
|
|
138
|
+
parser.print_help()
|
|
139
|
+
return 1
|
encino_orm/context.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Conexión por defecto / ambiente para `Model` sin `db` explícito.
|
|
2
|
+
|
|
3
|
+
Proporciona un "singleton" de conexión (por proceso) y un enlace ambiente por
|
|
4
|
+
tarea/contexto (`contextvars`), de modo que los `Model` puedan resolver su
|
|
5
|
+
conexión de forma implícita. El orden de resolución es:
|
|
6
|
+
|
|
7
|
+
1. `db` explícito (constructor/método).
|
|
8
|
+
2. transacción activa del pool (`_current_connection`).
|
|
9
|
+
3. `bind()` / `session()` (ambiente).
|
|
10
|
+
4. `set_default_db()` (proceso).
|
|
11
|
+
5. error `ConnectionError`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import contextvars
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
|
|
17
|
+
from .exceptions import ConnectionError
|
|
18
|
+
|
|
19
|
+
# Conexión/pool por defecto de TODO el proceso (el "singleton").
|
|
20
|
+
_default_db = None
|
|
21
|
+
|
|
22
|
+
# Conexión ambiente por tarea/contexto (bind / session).
|
|
23
|
+
_ambient_db = contextvars.ContextVar("encino_orm_ambient_db", default=None)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_default_db(db) -> None:
|
|
27
|
+
"""Registra la conexión o pool por defecto del proceso."""
|
|
28
|
+
global _default_db
|
|
29
|
+
_default_db = db
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_default_db():
|
|
33
|
+
"""Devuelve la conexión/pool por defecto del proceso, o `None`."""
|
|
34
|
+
return _default_db
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@contextmanager
|
|
38
|
+
def bind(db):
|
|
39
|
+
"""Establece la conexión ambiente para el bloque (async-safe vía contextvar)."""
|
|
40
|
+
token = _ambient_db.set(db)
|
|
41
|
+
try:
|
|
42
|
+
yield db
|
|
43
|
+
finally:
|
|
44
|
+
_ambient_db.reset(token)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def resolve_db():
|
|
48
|
+
"""Resuelve la conexión actual. Lanza `ConnectionError` si no hay ninguna."""
|
|
49
|
+
from .pool import _current_connection # lazy: evita import circular
|
|
50
|
+
|
|
51
|
+
conn = _current_connection.get() # 1. transacción activa del pool
|
|
52
|
+
if conn is not None:
|
|
53
|
+
return conn
|
|
54
|
+
ambient = _ambient_db.get() # 2. bind()/session()
|
|
55
|
+
if ambient is not None:
|
|
56
|
+
return ambient
|
|
57
|
+
if _default_db is not None: # 3. set_default_db()
|
|
58
|
+
return _default_db
|
|
59
|
+
raise ConnectionError(
|
|
60
|
+
"Sin conexión: pasa `db`, usa `bind()`, `set_default_db()` o `session()`"
|
|
61
|
+
)
|
encino_orm/engine.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Identificación tipada de motores de base de datos."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Engine(str, Enum):
|
|
7
|
+
"""Motores soportados (miembro `str`, comparable con su valor).
|
|
8
|
+
|
|
9
|
+
``Engine.SQLITE == "sqlite"`` es `True`; para claves de dict/parámetros que
|
|
10
|
+
exigen `str` usa ``engine.value``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
SQLITE = "sqlite"
|
|
14
|
+
MYSQL = "mysql"
|
|
15
|
+
POSTGRESQL = "postgresql"
|
|
16
|
+
|
|
17
|
+
def __str__(self) -> str:
|
|
18
|
+
return self.value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def engine_of(db) -> Engine:
|
|
22
|
+
"""Devuelve el motor activo como `Engine`.
|
|
23
|
+
|
|
24
|
+
Acepta un `Db`/`PoolDb` (lee `dialect`), un `Engine` (se devuelve tal cual) o
|
|
25
|
+
una cadena (`"sqlite"`/`"mysql"`/`"postgresql"`). Un valor desconocido lanza
|
|
26
|
+
`ValueError`.
|
|
27
|
+
"""
|
|
28
|
+
if isinstance(db, Engine):
|
|
29
|
+
return db
|
|
30
|
+
dialect = getattr(db, "dialect", db)
|
|
31
|
+
if isinstance(dialect, Engine):
|
|
32
|
+
return dialect
|
|
33
|
+
return Engine(dialect)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_sqlite(db) -> bool:
|
|
37
|
+
return engine_of(db) is Engine.SQLITE
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_mysql(db) -> bool:
|
|
41
|
+
return engine_of(db) is Engine.MYSQL
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_postgres(db) -> bool:
|
|
45
|
+
return engine_of(db) is Engine.POSTGRESQL
|
encino_orm/exceptions.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class EncinoOrmError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConnectionError(EncinoOrmError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class QueryError(EncinoOrmError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnsupportedEngineError(EncinoOrmError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MigrationError(EncinoOrmError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PoolExhaustedError(EncinoOrmError):
|
|
22
|
+
pass
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Subpaquete opcional `encino_orm.graphql` (Strawberry GraphQL).
|
|
2
|
+
|
|
3
|
+
Genera tipos, queries y mutations a partir de los `Model` de encino_orm.
|
|
4
|
+
`strawberry-graphql` es dependencia opcional (extras `graphql`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .schema import build_schema
|
|
8
|
+
|
|
9
|
+
__all__ = ["build_schema"]
|