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.
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