omlish 0.0.0.dev237__py3-none-any.whl → 0.0.0.dev239__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.
- omlish/__about__.py +2 -2
- omlish/collections/__init__.py +5 -5
- omlish/collections/ranked.py +79 -0
- omlish/configs/classes.py +4 -0
- omlish/daemons/services.py +74 -0
- omlish/daemons/targets.py +10 -22
- omlish/dataclasses/__init__.py +6 -2
- omlish/dataclasses/static.py +189 -0
- omlish/dataclasses/utils.py +0 -9
- omlish/graphs/trees.py +8 -8
- omlish/lang/__init__.py +6 -3
- omlish/lang/descriptors.py +8 -7
- omlish/lang/imports.py +0 -43
- omlish/lite/dataclasses.py +18 -0
- omlish/lite/imports.py +47 -0
- omlish/manifests/__init__.py +0 -2
- omlish/manifests/base.py +1 -0
- omlish/manifests/load.py +1 -0
- omlish/manifests/static.py +20 -0
- omlish/manifests/types.py +2 -1
- omlish/metadata.py +153 -0
- omlish/sql/abc.py +7 -0
- omlish/sql/api/__init__.py +0 -0
- omlish/sql/api/base.py +89 -0
- omlish/sql/api/columns.py +90 -0
- omlish/sql/api/dbapi.py +105 -0
- omlish/sql/api/errors.py +24 -0
- omlish/sql/api/funcs.py +73 -0
- omlish/sql/api/queries.py +63 -0
- omlish/sql/api/rows.py +48 -0
- {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/RECORD +36 -24
- omlish/collections/indexed.py +0 -73
- {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/top_level.txt +0 -0
omlish/lite/dataclasses.py
CHANGED
@@ -3,6 +3,18 @@ import dataclasses as dc
|
|
3
3
|
import typing as ta
|
4
4
|
|
5
5
|
|
6
|
+
##
|
7
|
+
|
8
|
+
|
9
|
+
def is_immediate_dataclass(cls: type) -> bool:
|
10
|
+
if not isinstance(cls, type):
|
11
|
+
raise TypeError(cls)
|
12
|
+
return dc._FIELDS in cls.__dict__ # type: ignore[attr-defined] # noqa
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
|
17
|
+
|
6
18
|
def dataclass_cache_hash(
|
7
19
|
*,
|
8
20
|
cached_hash_attr: str = '__dataclass_hash__',
|
@@ -33,6 +45,9 @@ def dataclass_cache_hash(
|
|
33
45
|
return inner
|
34
46
|
|
35
47
|
|
48
|
+
##
|
49
|
+
|
50
|
+
|
36
51
|
def dataclass_maybe_post_init(sup: ta.Any) -> bool:
|
37
52
|
if not isinstance(sup, super):
|
38
53
|
raise TypeError(sup)
|
@@ -44,6 +59,9 @@ def dataclass_maybe_post_init(sup: ta.Any) -> bool:
|
|
44
59
|
return True
|
45
60
|
|
46
61
|
|
62
|
+
##
|
63
|
+
|
64
|
+
|
47
65
|
def dataclass_repr_filtered(
|
48
66
|
obj: ta.Any,
|
49
67
|
fn: ta.Callable[[ta.Any, dc.Field, ta.Any], bool],
|
omlish/lite/imports.py
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import types
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
|
6
|
+
##
|
7
|
+
|
8
|
+
|
9
|
+
def import_module(dotted_path: str) -> types.ModuleType:
|
10
|
+
if not dotted_path:
|
11
|
+
raise ImportError(dotted_path)
|
12
|
+
mod = __import__(dotted_path, globals(), locals(), [])
|
13
|
+
for name in dotted_path.split('.')[1:]:
|
14
|
+
try:
|
15
|
+
mod = getattr(mod, name)
|
16
|
+
except AttributeError:
|
17
|
+
raise AttributeError(f'Module {mod!r} has no attribute {name!r}') from None
|
18
|
+
return mod
|
19
|
+
|
20
|
+
|
21
|
+
def import_module_attr(dotted_path: str) -> ta.Any:
|
22
|
+
module_name, _, class_name = dotted_path.rpartition('.')
|
23
|
+
mod = import_module(module_name)
|
24
|
+
try:
|
25
|
+
return getattr(mod, class_name)
|
26
|
+
except AttributeError:
|
27
|
+
raise AttributeError(f'Module {module_name!r} has no attr {class_name!r}') from None
|
28
|
+
|
29
|
+
|
30
|
+
def import_attr(dotted_path: str) -> ta.Any:
|
31
|
+
importlib = __import__('importlib')
|
32
|
+
parts = dotted_path.split('.')
|
33
|
+
mod: ta.Any = None
|
34
|
+
mod_pos = 0
|
35
|
+
while mod_pos < len(parts):
|
36
|
+
mod_name = '.'.join(parts[:mod_pos + 1])
|
37
|
+
try:
|
38
|
+
mod = importlib.import_module(mod_name)
|
39
|
+
except ImportError:
|
40
|
+
break
|
41
|
+
mod_pos += 1
|
42
|
+
if mod is None:
|
43
|
+
raise ImportError(dotted_path)
|
44
|
+
obj = mod
|
45
|
+
for att_pos in range(mod_pos, len(parts)):
|
46
|
+
obj = getattr(obj, parts[att_pos])
|
47
|
+
return obj
|
omlish/manifests/__init__.py
CHANGED
omlish/manifests/base.py
CHANGED
omlish/manifests/load.py
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
import abc
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
from .. import dataclasses as dc
|
5
|
+
from .. import lang
|
6
|
+
from .base import ModAttrManifest
|
7
|
+
|
8
|
+
|
9
|
+
##
|
10
|
+
|
11
|
+
|
12
|
+
class StaticModAttrManifest(dc.Static, ModAttrManifest, abc.ABC):
|
13
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
14
|
+
if (
|
15
|
+
not (lang.is_abstract_class(cls) or abc.ABC in cls.__bases__) and
|
16
|
+
'mod_name' not in cls.__dict__
|
17
|
+
):
|
18
|
+
setattr(cls, 'mod_name', cls.__module__)
|
19
|
+
|
20
|
+
super().__init_subclass__(**kwargs)
|
omlish/manifests/types.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
2
3
|
import dataclasses as dc
|
3
4
|
import typing as ta
|
4
5
|
|
@@ -6,7 +7,7 @@ import typing as ta
|
|
6
7
|
@dc.dataclass(frozen=True)
|
7
8
|
class ManifestOrigin:
|
8
9
|
module: str
|
9
|
-
attr: str
|
10
|
+
attr: ta.Optional[str] # None if inline
|
10
11
|
|
11
12
|
file: str
|
12
13
|
line: int
|
omlish/metadata.py
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
"""
|
2
|
+
These attempt to punch through *all* wrappers. The the usecases involve having things like bound methods, classmethods,
|
3
|
+
functools.partial instances, and needing to get the metadata of the underlying thing the end-user wrote.
|
4
|
+
|
5
|
+
TODO:
|
6
|
+
- re type targets:
|
7
|
+
- unwrap instances of objects to their types?
|
8
|
+
- merge mro?
|
9
|
+
- are these better left up to callers? too usecase-specific to favor either way?
|
10
|
+
"""
|
11
|
+
import types
|
12
|
+
import typing as ta
|
13
|
+
|
14
|
+
from . import check
|
15
|
+
from . import lang
|
16
|
+
|
17
|
+
|
18
|
+
T = ta.TypeVar('T')
|
19
|
+
|
20
|
+
|
21
|
+
##
|
22
|
+
|
23
|
+
|
24
|
+
class ObjectMetadata(lang.Abstract):
|
25
|
+
pass
|
26
|
+
|
27
|
+
|
28
|
+
class ObjectMetadataTarget(lang.Abstract):
|
29
|
+
pass
|
30
|
+
|
31
|
+
|
32
|
+
##
|
33
|
+
|
34
|
+
|
35
|
+
_VALID_OBJECT_METADATA_TARGET_TYPES: tuple[type, ...] = (
|
36
|
+
type,
|
37
|
+
|
38
|
+
types.FunctionType,
|
39
|
+
# *not* types.MethodType - must unwrap to unbound class func
|
40
|
+
# *not* functools.partial - must unwrap to underlying func
|
41
|
+
|
42
|
+
ObjectMetadataTarget,
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
class ObjectMetadataTargetTypeError(TypeError):
|
47
|
+
pass
|
48
|
+
|
49
|
+
|
50
|
+
def _unwrap_object_metadata_target(obj: ta.Any) -> ta.Any:
|
51
|
+
tgt: ta.Any = obj
|
52
|
+
tgt = lang.unwrap_func(tgt)
|
53
|
+
|
54
|
+
if not isinstance(tgt, _VALID_OBJECT_METADATA_TARGET_TYPES):
|
55
|
+
raise ObjectMetadataTargetTypeError(tgt)
|
56
|
+
|
57
|
+
return tgt
|
58
|
+
|
59
|
+
|
60
|
+
##
|
61
|
+
|
62
|
+
|
63
|
+
_OBJECT_METADATA_ATTR = '__' + __name__.replace('.', '_') + '__metadata__'
|
64
|
+
|
65
|
+
|
66
|
+
def append_object_metadata(obj: T, *mds: ObjectMetadata) -> T:
|
67
|
+
for md in mds:
|
68
|
+
check.isinstance(md, ObjectMetadata)
|
69
|
+
|
70
|
+
tgt = _unwrap_object_metadata_target(obj)
|
71
|
+
dct = tgt.__dict__
|
72
|
+
|
73
|
+
if isinstance(dct, types.MappingProxyType):
|
74
|
+
for _ in range(2):
|
75
|
+
try:
|
76
|
+
lst = dct[_OBJECT_METADATA_ATTR]
|
77
|
+
except KeyError:
|
78
|
+
setattr(tgt, _OBJECT_METADATA_ATTR, [])
|
79
|
+
else:
|
80
|
+
break
|
81
|
+
else:
|
82
|
+
raise RuntimeError
|
83
|
+
|
84
|
+
else:
|
85
|
+
lst = dct.setdefault(_OBJECT_METADATA_ATTR, [])
|
86
|
+
|
87
|
+
lst.extend(mds)
|
88
|
+
return obj
|
89
|
+
|
90
|
+
|
91
|
+
def get_object_metadata(obj: ta.Any, *, strict: bool = False) -> ta.Sequence[ObjectMetadata]:
|
92
|
+
try:
|
93
|
+
tgt = _unwrap_object_metadata_target(obj)
|
94
|
+
except ObjectMetadataTargetTypeError:
|
95
|
+
if not strict:
|
96
|
+
return ()
|
97
|
+
raise
|
98
|
+
|
99
|
+
try:
|
100
|
+
dct = tgt.__dict__
|
101
|
+
except AttributeError:
|
102
|
+
return ()
|
103
|
+
|
104
|
+
return dct.get(_OBJECT_METADATA_ATTR, ())
|
105
|
+
|
106
|
+
|
107
|
+
##
|
108
|
+
|
109
|
+
|
110
|
+
class DecoratorObjectMetadata(ObjectMetadata, lang.Abstract):
|
111
|
+
_OBJECT_METADATA_TARGET_TYPES: ta.ClassVar[tuple[type, ...] | None] = None
|
112
|
+
|
113
|
+
def __init_subclass__(
|
114
|
+
cls,
|
115
|
+
*,
|
116
|
+
object_metadata_target_types: ta.Iterable[type] | None = None,
|
117
|
+
**kwargs: ta.Any,
|
118
|
+
) -> None:
|
119
|
+
super().__init_subclass__(**kwargs)
|
120
|
+
|
121
|
+
if object_metadata_target_types is not None:
|
122
|
+
tts = tuple(object_metadata_target_types)
|
123
|
+
for tt in tts:
|
124
|
+
check.issubclass(tt, _VALID_OBJECT_METADATA_TARGET_TYPES)
|
125
|
+
setattr(cls, '_OBJECT_METADATA_TARGET_TYPES', tts)
|
126
|
+
|
127
|
+
def __call__(self, obj: T) -> T:
|
128
|
+
tgt: ta.Any = obj
|
129
|
+
if (tts := type(self)._OBJECT_METADATA_TARGET_TYPES) is not None: # noqa
|
130
|
+
tgt = _unwrap_object_metadata_target(tgt)
|
131
|
+
check.isinstance(tgt, tts)
|
132
|
+
|
133
|
+
append_object_metadata(tgt, self)
|
134
|
+
return obj
|
135
|
+
|
136
|
+
|
137
|
+
#
|
138
|
+
|
139
|
+
|
140
|
+
class ClassDecoratorObjectMetadata(
|
141
|
+
DecoratorObjectMetadata,
|
142
|
+
lang.Abstract,
|
143
|
+
object_metadata_target_types=[type],
|
144
|
+
):
|
145
|
+
pass
|
146
|
+
|
147
|
+
|
148
|
+
class FunctionDecoratorObjectMetadata(
|
149
|
+
DecoratorObjectMetadata,
|
150
|
+
lang.Abstract,
|
151
|
+
object_metadata_target_types=[types.FunctionType],
|
152
|
+
):
|
153
|
+
pass
|
omlish/sql/abc.py
CHANGED
@@ -26,6 +26,13 @@ class DbapiColumnDescription_(ta.NamedTuple): # noqa
|
|
26
26
|
scale: int | None
|
27
27
|
null_ok: bool | None
|
28
28
|
|
29
|
+
@classmethod
|
30
|
+
def of(cls, obj: ta.Any) -> 'DbapiColumnDescription_':
|
31
|
+
if isinstance(obj, cls):
|
32
|
+
return obj
|
33
|
+
else:
|
34
|
+
return cls(*obj[:len(cls._fields)])
|
35
|
+
|
29
36
|
|
30
37
|
class DbapiConnection(ta.Protocol):
|
31
38
|
def close(self) -> object: ...
|
File without changes
|
omlish/sql/api/base.py
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- sync vs async
|
4
|
+
"""
|
5
|
+
import abc
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from ... import lang
|
9
|
+
from .columns import Column
|
10
|
+
from .columns import Columns
|
11
|
+
from .queries import Query
|
12
|
+
from .rows import Row
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
|
17
|
+
|
18
|
+
class Closer(lang.Abstract):
|
19
|
+
def close(self) -> None:
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
class SelfCloser(Closer):
|
24
|
+
def __enter__(self) -> ta.Self:
|
25
|
+
return self
|
26
|
+
|
27
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
28
|
+
self.close()
|
29
|
+
|
30
|
+
|
31
|
+
##
|
32
|
+
|
33
|
+
|
34
|
+
class Querier(SelfCloser, lang.Abstract):
|
35
|
+
@property
|
36
|
+
@abc.abstractmethod
|
37
|
+
def adapter(self) -> 'Adapter':
|
38
|
+
raise NotImplementedError
|
39
|
+
|
40
|
+
@abc.abstractmethod
|
41
|
+
def query(self, query: Query) -> 'Rows': # ta.Raises[QueryError]
|
42
|
+
raise NotImplementedError
|
43
|
+
|
44
|
+
|
45
|
+
##
|
46
|
+
|
47
|
+
|
48
|
+
class Rows(SelfCloser, lang.Abstract):
|
49
|
+
@property
|
50
|
+
@abc.abstractmethod
|
51
|
+
def columns(self) -> Columns:
|
52
|
+
raise NotImplementedError
|
53
|
+
|
54
|
+
@ta.final
|
55
|
+
def __iter__(self) -> ta.Self:
|
56
|
+
return self
|
57
|
+
|
58
|
+
@abc.abstractmethod
|
59
|
+
def __next__(self) -> Row: # ta.Raises[StopIteration]
|
60
|
+
raise NotImplementedError
|
61
|
+
|
62
|
+
|
63
|
+
##
|
64
|
+
|
65
|
+
|
66
|
+
class Conn(Querier, lang.Abstract): # noqa
|
67
|
+
pass
|
68
|
+
|
69
|
+
|
70
|
+
##
|
71
|
+
|
72
|
+
|
73
|
+
class Db(Querier, lang.Abstract): # noqa
|
74
|
+
@abc.abstractmethod
|
75
|
+
def connect(self) -> Conn:
|
76
|
+
raise NotImplementedError
|
77
|
+
|
78
|
+
def query(self, query: Query) -> Rows:
|
79
|
+
with self.connect() as conn:
|
80
|
+
return conn.query(query)
|
81
|
+
|
82
|
+
|
83
|
+
##
|
84
|
+
|
85
|
+
|
86
|
+
class Adapter(lang.Abstract):
|
87
|
+
@abc.abstractmethod
|
88
|
+
def scan_type(self, c: Column) -> type:
|
89
|
+
raise NotImplementedError
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import dataclasses as dc
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
from ... import lang
|
5
|
+
from .errors import DuplicateColumnNameError
|
6
|
+
|
7
|
+
|
8
|
+
##
|
9
|
+
|
10
|
+
|
11
|
+
@dc.dataclass(frozen=True)
|
12
|
+
class Column(lang.Final):
|
13
|
+
name: str
|
14
|
+
|
15
|
+
_: dc.KW_ONLY
|
16
|
+
|
17
|
+
db_type: str | None = None
|
18
|
+
type: type = object
|
19
|
+
|
20
|
+
|
21
|
+
##
|
22
|
+
|
23
|
+
|
24
|
+
class Columns(lang.Final):
|
25
|
+
def __init__(self, *cs: Column) -> None:
|
26
|
+
super().__init__()
|
27
|
+
|
28
|
+
self._seq = cs
|
29
|
+
|
30
|
+
by_name: dict[str, Column] = {}
|
31
|
+
idxs_by_name: dict[str, int] = {}
|
32
|
+
for i, c in enumerate(cs):
|
33
|
+
if c.name in by_name:
|
34
|
+
raise DuplicateColumnNameError(c.name)
|
35
|
+
by_name[c.name] = c
|
36
|
+
idxs_by_name[c.name] = i
|
37
|
+
|
38
|
+
self._by_name = by_name
|
39
|
+
self._idxs_by_name = idxs_by_name
|
40
|
+
|
41
|
+
#
|
42
|
+
|
43
|
+
_EMPTY: ta.ClassVar['Columns']
|
44
|
+
|
45
|
+
@classmethod
|
46
|
+
def empty(cls) -> 'Columns':
|
47
|
+
return cls._EMPTY
|
48
|
+
|
49
|
+
#
|
50
|
+
|
51
|
+
def __repr__(self) -> str:
|
52
|
+
return f'{self.__class__.__name__}<{", ".join(repr(c.name) for c in self._seq)}>'
|
53
|
+
|
54
|
+
#
|
55
|
+
|
56
|
+
def __iter__(self) -> ta.Iterator[Column]:
|
57
|
+
return iter(self._seq)
|
58
|
+
|
59
|
+
def __len__(self) -> int:
|
60
|
+
return len(self._seq)
|
61
|
+
|
62
|
+
def __contains__(self, item: str | int) -> bool:
|
63
|
+
if isinstance(item, str):
|
64
|
+
return item in self._by_name
|
65
|
+
elif isinstance(item, int):
|
66
|
+
return 0 <= item < len(self._by_name)
|
67
|
+
else:
|
68
|
+
raise TypeError(item)
|
69
|
+
|
70
|
+
def __getitem__(self, item) -> Column:
|
71
|
+
if isinstance(item, str):
|
72
|
+
return self._by_name[item]
|
73
|
+
elif isinstance(item, int):
|
74
|
+
return self._seq[item]
|
75
|
+
else:
|
76
|
+
raise TypeError(item)
|
77
|
+
|
78
|
+
def get(self, name: str) -> Column | None:
|
79
|
+
return self._by_name.get(name)
|
80
|
+
|
81
|
+
#
|
82
|
+
|
83
|
+
def index(self, name: str) -> int:
|
84
|
+
return self._idxs_by_name[name]
|
85
|
+
|
86
|
+
def get_index(self, name: str) -> int | None:
|
87
|
+
return self._idxs_by_name.get(name)
|
88
|
+
|
89
|
+
|
90
|
+
Columns._EMPTY = Columns() # noqa
|
omlish/sql/api/dbapi.py
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
import typing as ta
|
2
|
+
|
3
|
+
from ... import check
|
4
|
+
from .. import abc as dbapi_abc
|
5
|
+
from .base import Adapter
|
6
|
+
from .base import Conn
|
7
|
+
from .base import Db
|
8
|
+
from .base import Rows
|
9
|
+
from .columns import Column
|
10
|
+
from .columns import Columns
|
11
|
+
from .queries import Query
|
12
|
+
from .rows import Row
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
|
17
|
+
|
18
|
+
def build_dbapi_columns(desc: ta.Sequence[dbapi_abc.DbapiColumnDescription] | None) -> Columns:
|
19
|
+
if desc is None:
|
20
|
+
return Columns.empty()
|
21
|
+
|
22
|
+
cols: list[Column] = []
|
23
|
+
for desc_col in desc:
|
24
|
+
dbapi_col = dbapi_abc.DbapiColumnDescription_.of(desc_col)
|
25
|
+
|
26
|
+
cols.append(Column(
|
27
|
+
check.non_empty_str(dbapi_col.name),
|
28
|
+
))
|
29
|
+
|
30
|
+
return Columns(*cols)
|
31
|
+
|
32
|
+
|
33
|
+
class DbapiRows(Rows):
|
34
|
+
def __init__(
|
35
|
+
self,
|
36
|
+
cursor: dbapi_abc.DbapiCursor,
|
37
|
+
columns: Columns,
|
38
|
+
) -> None:
|
39
|
+
super().__init__()
|
40
|
+
|
41
|
+
self._cursor = cursor
|
42
|
+
self._columns = columns
|
43
|
+
|
44
|
+
@property
|
45
|
+
def columns(self) -> Columns:
|
46
|
+
return self._columns
|
47
|
+
|
48
|
+
def __next__(self) -> Row:
|
49
|
+
values = self._cursor.fetchone()
|
50
|
+
if values is None:
|
51
|
+
raise StopIteration
|
52
|
+
return Row(self._columns, values)
|
53
|
+
|
54
|
+
|
55
|
+
class DbapiConn(Conn):
|
56
|
+
def __init__(self, conn: dbapi_abc.DbapiConnection) -> None:
|
57
|
+
super().__init__()
|
58
|
+
|
59
|
+
self._conn = conn
|
60
|
+
|
61
|
+
@property
|
62
|
+
def adapter(self) -> Adapter:
|
63
|
+
raise NotImplementedError
|
64
|
+
|
65
|
+
def query(self, query: Query) -> Rows:
|
66
|
+
cursor = self._conn.cursor()
|
67
|
+
try:
|
68
|
+
cursor.execute(query.text)
|
69
|
+
columns = build_dbapi_columns(cursor.description)
|
70
|
+
return DbapiRows(cursor, columns)
|
71
|
+
|
72
|
+
except Exception: # noqa
|
73
|
+
cursor.close()
|
74
|
+
raise
|
75
|
+
|
76
|
+
def close(self) -> None:
|
77
|
+
self._conn.close()
|
78
|
+
|
79
|
+
|
80
|
+
class DbapiDb(Db):
|
81
|
+
def __init__(
|
82
|
+
self,
|
83
|
+
conn_fac: ta.Callable[[], dbapi_abc.DbapiConnection],
|
84
|
+
*,
|
85
|
+
adapter: ta.Optional['DbapiAdapter'] = None,
|
86
|
+
) -> None:
|
87
|
+
super().__init__()
|
88
|
+
|
89
|
+
self._conn_fac = conn_fac
|
90
|
+
if adapter is None:
|
91
|
+
adapter = DbapiAdapter()
|
92
|
+
self._adapter = adapter
|
93
|
+
|
94
|
+
def connect(self) -> Conn:
|
95
|
+
dbapi_conn = self._conn_fac()
|
96
|
+
return DbapiConn(dbapi_conn)
|
97
|
+
|
98
|
+
@property
|
99
|
+
def adapter(self) -> Adapter:
|
100
|
+
return self._adapter
|
101
|
+
|
102
|
+
|
103
|
+
class DbapiAdapter(Adapter):
|
104
|
+
def scan_type(self, c: Column) -> type:
|
105
|
+
raise NotImplementedError
|
omlish/sql/api/errors.py
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
class Error(Exception):
|
2
|
+
pass
|
3
|
+
|
4
|
+
|
5
|
+
##
|
6
|
+
|
7
|
+
|
8
|
+
class ColumnError(Error):
|
9
|
+
pass
|
10
|
+
|
11
|
+
|
12
|
+
class DuplicateColumnNameError(ColumnError):
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
class MismatchedColumnCountError(ColumnError):
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
##
|
21
|
+
|
22
|
+
|
23
|
+
class QueryError(Error):
|
24
|
+
pass
|
omlish/sql/api/funcs.py
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
import typing as ta
|
2
|
+
|
3
|
+
from .base import Querier
|
4
|
+
from .base import Rows
|
5
|
+
from .queries import Query
|
6
|
+
from .queries import QueryMode
|
7
|
+
|
8
|
+
|
9
|
+
##
|
10
|
+
|
11
|
+
|
12
|
+
@ta.overload
|
13
|
+
def query(
|
14
|
+
querier: Querier,
|
15
|
+
query: Query, # noqa
|
16
|
+
) -> Rows:
|
17
|
+
...
|
18
|
+
|
19
|
+
|
20
|
+
@ta.overload
|
21
|
+
def query(
|
22
|
+
querier: Querier,
|
23
|
+
text: str,
|
24
|
+
*args: ta.Any,
|
25
|
+
) -> Rows:
|
26
|
+
...
|
27
|
+
|
28
|
+
|
29
|
+
def query(
|
30
|
+
querier,
|
31
|
+
obj,
|
32
|
+
*args,
|
33
|
+
):
|
34
|
+
if isinstance(obj, Query):
|
35
|
+
q = obj
|
36
|
+
else:
|
37
|
+
q = Query.of(obj, *args, mode=QueryMode.QUERY)
|
38
|
+
|
39
|
+
return querier.query(q)
|
40
|
+
|
41
|
+
|
42
|
+
##
|
43
|
+
|
44
|
+
|
45
|
+
@ta.overload
|
46
|
+
def exec( # noqa
|
47
|
+
querier: Querier,
|
48
|
+
query: Query, # noqa
|
49
|
+
) -> None:
|
50
|
+
...
|
51
|
+
|
52
|
+
|
53
|
+
@ta.overload
|
54
|
+
def exec( # noqa
|
55
|
+
querier: Querier,
|
56
|
+
text: str,
|
57
|
+
*args: ta.Any,
|
58
|
+
) -> None:
|
59
|
+
...
|
60
|
+
|
61
|
+
|
62
|
+
def exec( # noqa
|
63
|
+
querier,
|
64
|
+
obj,
|
65
|
+
*args,
|
66
|
+
):
|
67
|
+
if isinstance(obj, Query):
|
68
|
+
q = obj
|
69
|
+
else:
|
70
|
+
q = Query.of(obj, *args, mode=QueryMode.EXEC)
|
71
|
+
|
72
|
+
with querier.query(q):
|
73
|
+
pass
|