omlish 0.0.0.dev237__py3-none-any.whl → 0.0.0.dev239__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. omlish/__about__.py +2 -2
  2. omlish/collections/__init__.py +5 -5
  3. omlish/collections/ranked.py +79 -0
  4. omlish/configs/classes.py +4 -0
  5. omlish/daemons/services.py +74 -0
  6. omlish/daemons/targets.py +10 -22
  7. omlish/dataclasses/__init__.py +6 -2
  8. omlish/dataclasses/static.py +189 -0
  9. omlish/dataclasses/utils.py +0 -9
  10. omlish/graphs/trees.py +8 -8
  11. omlish/lang/__init__.py +6 -3
  12. omlish/lang/descriptors.py +8 -7
  13. omlish/lang/imports.py +0 -43
  14. omlish/lite/dataclasses.py +18 -0
  15. omlish/lite/imports.py +47 -0
  16. omlish/manifests/__init__.py +0 -2
  17. omlish/manifests/base.py +1 -0
  18. omlish/manifests/load.py +1 -0
  19. omlish/manifests/static.py +20 -0
  20. omlish/manifests/types.py +2 -1
  21. omlish/metadata.py +153 -0
  22. omlish/sql/abc.py +7 -0
  23. omlish/sql/api/__init__.py +0 -0
  24. omlish/sql/api/base.py +89 -0
  25. omlish/sql/api/columns.py +90 -0
  26. omlish/sql/api/dbapi.py +105 -0
  27. omlish/sql/api/errors.py +24 -0
  28. omlish/sql/api/funcs.py +73 -0
  29. omlish/sql/api/queries.py +63 -0
  30. omlish/sql/api/rows.py +48 -0
  31. {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/METADATA +1 -1
  32. {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/RECORD +36 -24
  33. omlish/collections/indexed.py +0 -73
  34. {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/LICENSE +0 -0
  35. {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/WHEEL +0 -0
  36. {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/entry_points.txt +0 -0
  37. {omlish-0.0.0.dev237.dist-info → omlish-0.0.0.dev239.dist-info}/top_level.txt +0 -0
@@ -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
@@ -1,2 +0,0 @@
1
- # @omlish-lite
2
- from .types import Manifest # noqa
omlish/manifests/base.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
 
omlish/manifests/load.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
2
3
  """
3
4
  Should be kept somewhat lightweight - used in cli entrypoints.
4
5
 
@@ -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
@@ -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
@@ -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
@@ -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