python-corekit 0.1.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.
Files changed (125) hide show
  1. corekit/__init__.py +0 -0
  2. corekit/api/__init__.py +9 -0
  3. corekit/api/handler.py +76 -0
  4. corekit/api/responses.py +40 -0
  5. corekit/api/routers.py +115 -0
  6. corekit/concurrency/__init__.py +9 -0
  7. corekit/concurrency/decorators.py +72 -0
  8. corekit/concurrency/thread_local.py +99 -0
  9. corekit/concurrency/worker.py +65 -0
  10. corekit/config/__init__.py +47 -0
  11. corekit/config/loader.py +153 -0
  12. corekit/config/settings.py +161 -0
  13. corekit/config/sources.py +125 -0
  14. corekit/connections/__init__.py +31 -0
  15. corekit/connections/connectable.py +212 -0
  16. corekit/connections/decorators.py +92 -0
  17. corekit/connections/redis/__init__.py +7 -0
  18. corekit/connections/redis/connection.py +239 -0
  19. corekit/connections/registry.py +80 -0
  20. corekit/connections/sql/__init__.py +10 -0
  21. corekit/connections/sql/connection.py +342 -0
  22. corekit/connections/sql/fields/__init__.py +7 -0
  23. corekit/connections/sql/fields/jsonb.py +67 -0
  24. corekit/connections/sql/migration/__init__.py +57 -0
  25. corekit/connections/sql/migration/base.py +40 -0
  26. corekit/connections/sql/migration/operations.py +416 -0
  27. corekit/connections/sql/migration/registry.py +166 -0
  28. corekit/connections/sql/migration/table.py +27 -0
  29. corekit/connections/sql/query.py +68 -0
  30. corekit/connections/sql/table.py +96 -0
  31. corekit/constants.py +45 -0
  32. corekit/crypto/__init__.py +1 -0
  33. corekit/crypto/constants.py +7 -0
  34. corekit/crypto/enum.py +11 -0
  35. corekit/crypto/hasher.py +89 -0
  36. corekit/data/__init__.py +81 -0
  37. corekit/data/dataset.py +340 -0
  38. corekit/data/expressions/__init__.py +46 -0
  39. corekit/data/expressions/comparison.py +252 -0
  40. corekit/data/expressions/expression.py +98 -0
  41. corekit/data/record.py +147 -0
  42. corekit/data/stats.py +157 -0
  43. corekit/decorators/__init__.py +2 -0
  44. corekit/decorators/exception_handling.py +43 -0
  45. corekit/decorators/warnings.py +35 -0
  46. corekit/docker/__init__.py +7 -0
  47. corekit/docker/watchdog.py +222 -0
  48. corekit/etl/__init__.py +44 -0
  49. corekit/etl/connection.py +44 -0
  50. corekit/etl/extract/__init__.py +0 -0
  51. corekit/etl/extract/extractor.py +48 -0
  52. corekit/etl/extract/schemas.py +18 -0
  53. corekit/etl/load/__init__.py +0 -0
  54. corekit/etl/load/loader.py +53 -0
  55. corekit/etl/load/schemas.py +33 -0
  56. corekit/etl/orchestrator.py +201 -0
  57. corekit/etl/schemas.py +22 -0
  58. corekit/etl/transform/__init__.py +0 -0
  59. corekit/etl/transform/schemas.py +15 -0
  60. corekit/etl/transform/transformer.py +28 -0
  61. corekit/events/__init__.py +38 -0
  62. corekit/events/enum.py +58 -0
  63. corekit/events/frames.py +51 -0
  64. corekit/events/models.py +23 -0
  65. corekit/events/publisher.py +75 -0
  66. corekit/events/reader.py +132 -0
  67. corekit/events/sse.py +109 -0
  68. corekit/events/websocket.py +97 -0
  69. corekit/exceptions/__init__.py +0 -0
  70. corekit/exceptions/base.py +45 -0
  71. corekit/exceptions/custom/__init__.py +0 -0
  72. corekit/exceptions/http/__init__.py +0 -0
  73. corekit/exceptions/http/exceptions.py +37 -0
  74. corekit/exceptions/types.py +17 -0
  75. corekit/files/__init__.py +25 -0
  76. corekit/files/base.py +117 -0
  77. corekit/files/enum.py +30 -0
  78. corekit/files/json.py +12 -0
  79. corekit/files/pickle.py +12 -0
  80. corekit/files/toml.py +43 -0
  81. corekit/http/__init__.py +0 -0
  82. corekit/http/client.py +176 -0
  83. corekit/http/exponential_backoff.py +100 -0
  84. corekit/http/response.py +12 -0
  85. corekit/log_monitor/__init__.py +23 -0
  86. corekit/log_monitor/constants.py +8 -0
  87. corekit/log_monitor/models.py +150 -0
  88. corekit/log_monitor/service.py +418 -0
  89. corekit/notifications/__init__.py +8 -0
  90. corekit/notifications/base.py +51 -0
  91. corekit/notifications/models.py +34 -0
  92. corekit/observability/__init__.py +21 -0
  93. corekit/observability/benchmarkable.py +12 -0
  94. corekit/observability/loggable.py +29 -0
  95. corekit/observability/timing/__init__.py +0 -0
  96. corekit/observability/timing/constants.py +1 -0
  97. corekit/observability/timing/split.py +20 -0
  98. corekit/observability/timing/timer.py +30 -0
  99. corekit/py.typed +0 -0
  100. corekit/registry/__init__.py +12 -0
  101. corekit/registry/registry.py +134 -0
  102. corekit/schemas/__init__.py +0 -0
  103. corekit/schemas/dataclasses/__init__.py +0 -0
  104. corekit/schemas/enum.py +49 -0
  105. corekit/schemas/models/__init__.py +0 -0
  106. corekit/schemas/models/arbitrary.py +11 -0
  107. corekit/schemas/models/date_models.py +18 -0
  108. corekit/schemas/pydantic/__init__.py +0 -0
  109. corekit/schemas/pydantic/fields.py +35 -0
  110. corekit/schemas/types.py +40 -0
  111. corekit/serialization/__init__.py +0 -0
  112. corekit/serialization/enum.py +21 -0
  113. corekit/serialization/serializable.py +42 -0
  114. corekit/serialization/serializer.py +179 -0
  115. corekit/utils/__init__.py +5 -0
  116. corekit/utils/ids.py +5 -0
  117. corekit/utils/raise_exc.py +8 -0
  118. corekit/utils/time.py +21 -0
  119. corekit/utils/validators.py +15 -0
  120. corekit/utils/void.py +8 -0
  121. python_corekit-0.1.0.dist-info/METADATA +417 -0
  122. python_corekit-0.1.0.dist-info/RECORD +125 -0
  123. python_corekit-0.1.0.dist-info/WHEEL +5 -0
  124. python_corekit-0.1.0.dist-info/licenses/LICENSE +21 -0
  125. python_corekit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,166 @@
1
+ """
2
+ Discovering, verifying and running migrations.
3
+
4
+ On startup the registry checks that every previously-applied migration still
5
+ matches the checksum it was applied under, then runs whatever is pending in
6
+ version order.
7
+
8
+ The checksum is taken over each operation's ``canonical()`` -- a structural
9
+ description of what the operation does -- rather than over its rendered SQL.
10
+ Reformatting the generated SQL therefore leaves history valid, while changing
11
+ what a migration actually does invalidates it. A mismatch stops startup rather
12
+ than being repaired silently, because a migration that has already run cannot
13
+ be un-run by editing its source.
14
+ """
15
+
16
+ import importlib
17
+ import inspect
18
+ import pkgutil
19
+ from types import ModuleType
20
+
21
+ from corekit.connections.sql.connection import SQLConnection
22
+ from corekit.connections.sql.migration.base import Migration
23
+ from corekit.connections.sql.migration.operations import DataOperation
24
+ from corekit.connections.sql.migration.table import SchemaMigration
25
+ from corekit.observability.benchmarkable import Benchmarkable
26
+
27
+ __all__ = ["MigrationChecksumError", "MigrationRegistry"]
28
+
29
+
30
+ class MigrationChecksumError(RuntimeError):
31
+ """
32
+ Raised when an applied migration no longer matches its recorded checksum.
33
+ """
34
+
35
+
36
+ class MigrationRegistry(Benchmarkable):
37
+ """
38
+ An ordered, validated set of migrations.
39
+ """
40
+
41
+ def __init__(self, *migrations: Migration) -> None:
42
+ super().__init__()
43
+ self._migrations = self._validate(list(migrations))
44
+
45
+ @property
46
+ def migrations(self) -> list[Migration]:
47
+ """
48
+ The migrations, in version order.
49
+ """
50
+ return list(self._migrations)
51
+
52
+ @classmethod
53
+ def discover(cls, package: ModuleType) -> "MigrationRegistry":
54
+ """
55
+ Build a registry from every Migration subclass defined in a package.
56
+
57
+ Adding a file to that package is the whole registration step.
58
+ """
59
+ migrations: list[Migration] = []
60
+ for module_info in pkgutil.iter_modules(package.__path__, prefix=f"{package.__name__}."):
61
+ module = importlib.import_module(module_info.name)
62
+ for _, obj in inspect.getmembers(module, inspect.isclass):
63
+ if obj is Migration or not issubclass(obj, Migration) or obj.__module__ != module.__name__:
64
+ continue
65
+ migrations.append(obj())
66
+ return cls(*migrations)
67
+
68
+ @staticmethod
69
+ def _validate(migrations: list[Migration]) -> list[Migration]:
70
+ """
71
+ Reject duplicates and incomplete definitions, and sort by version.
72
+ """
73
+ seen_versions: set[int] = set()
74
+ for migration in migrations:
75
+ # version and name are annotated on the base class but not
76
+ # assigned, so an undeclared one raises AttributeError rather than
77
+ # returning the default. Repr is avoided for the same reason.
78
+ version = getattr(migration, "version", None)
79
+ if version is None or getattr(migration, "name", None) is None:
80
+ raise ValueError(f"Migration {type(migration).__name__} is missing version or name.")
81
+ if version in seen_versions:
82
+ raise ValueError(f"Duplicate migration version: {migration.version}")
83
+ seen_versions.add(version)
84
+ return sorted(migrations, key=lambda m: m.version)
85
+
86
+ def _fetch_applied(self, conn: SQLConnection) -> dict[int, str]:
87
+ """
88
+ Return ``{version: checksum}`` for everything already applied.
89
+ """
90
+ return {row.version: row.checksum for row in conn.fetch_all(SchemaMigration)}
91
+
92
+ def _verify_checksums(self, applied: dict[int, str]) -> None:
93
+ """
94
+ Confirm no applied migration has been edited since it ran.
95
+ """
96
+ for migration in self._migrations:
97
+ stored = applied.get(migration.version)
98
+ if stored is None or stored == migration.checksum:
99
+ continue
100
+ raise MigrationChecksumError(
101
+ f"\n\nMIGRATION CHECKSUM MISMATCH - REFUSING TO START\n"
102
+ f" Migration : v{migration.version} ({migration.name})\n"
103
+ f" Stored : {stored}\n"
104
+ f" Current : {migration.checksum}\n"
105
+ f"This migration was modified after it was applied to the database.\n"
106
+ f"Restore it to its original content, or add a new migration instead.\n"
107
+ )
108
+
109
+ def pending(self, conn: SQLConnection) -> list[Migration]:
110
+ """
111
+ The migrations not yet applied to this database.
112
+ """
113
+ applied = self._fetch_applied(conn)
114
+ return [m for m in self._migrations if m.version not in applied]
115
+
116
+ def _run_migration(self, conn: SQLConnection, migration: Migration) -> None:
117
+ """
118
+ Apply one migration and record it.
119
+ """
120
+ self.info(f"Applying migration v{migration.version}: {migration.name}")
121
+ for operation in migration.operations():
122
+ self.info(f" {operation.canonical()}")
123
+ if isinstance(operation, DataOperation):
124
+ operation.execute(conn)
125
+ else:
126
+ conn.exec_ddl(operation.to_sql())
127
+
128
+ conn.insert(
129
+ SchemaMigration(
130
+ version=migration.version,
131
+ name=migration.name,
132
+ checksum=migration.checksum,
133
+ )
134
+ )
135
+ self.info(f"Migration v{migration.version} applied.")
136
+
137
+ def run(self, conn: SQLConnection) -> None:
138
+ """
139
+ Verify history, then apply anything pending.
140
+
141
+ A failing migration stops the run: later migrations are written against
142
+ the schema earlier ones produce, so continuing past a failure would
143
+ apply them to a schema they were never written for.
144
+
145
+ Note that DDL is committed per statement, so a migration that fails
146
+ part-way leaves its earlier operations applied but the migration itself
147
+ unrecorded. Prefer several small migrations over one large one, and
148
+ check the schema before re-running after a failure.
149
+ """
150
+ applied = self._fetch_applied(conn)
151
+ self._verify_checksums(applied)
152
+
153
+ pending = [m for m in self._migrations if m.version not in applied]
154
+ if not pending:
155
+ self.info("Database schema is up to date.")
156
+ return
157
+
158
+ self.info(f"{len(pending)} migration(s) pending.")
159
+ for migration in pending:
160
+ try:
161
+ self._run_migration(conn, migration)
162
+ except MigrationChecksumError:
163
+ raise
164
+ except Exception as exc:
165
+ self.error(f"Migration v{migration.version} ({migration.name}) failed: {exc}")
166
+ raise
@@ -0,0 +1,27 @@
1
+ """
2
+ The table recording which migrations have been applied.
3
+ """
4
+
5
+ from datetime import datetime
6
+
7
+ from sqlmodel import Field, SQLModel
8
+
9
+ from corekit.utils.time import time_now
10
+
11
+ __all__ = ["SchemaMigration"]
12
+
13
+
14
+ class SchemaMigration(SQLModel, table=True):
15
+ """
16
+ One row per applied migration, with the checksum it was applied under.
17
+
18
+ Inherits SQLModel rather than NamedTable: this table is keyed by version,
19
+ and has no string id for NamedTable's registry to address it by.
20
+ """
21
+
22
+ __tablename__ = "schema_migration"
23
+
24
+ version: int = Field(primary_key=True)
25
+ name: str
26
+ checksum: str
27
+ applied_at: datetime = Field(default_factory=time_now)
@@ -0,0 +1,68 @@
1
+ """
2
+ A chainable query builder over SQLModel's select().
3
+ """
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+ from sqlmodel import desc as _desc
9
+ from sqlmodel import select
10
+
11
+ from corekit.connections.sql.table import NamedTable
12
+
13
+ __all__ = ["Query"]
14
+
15
+
16
+ class Query(BaseModel):
17
+ """
18
+ Accumulates filters, ordering and a limit, then builds a select statement.
19
+
20
+ Query(table=User).where(User.age >= 18).order_by(User.name).limit(10)
21
+ """
22
+
23
+ table: type[NamedTable]
24
+ queries: list[Any] = Field(default_factory=list)
25
+ order_by_field: Any = None
26
+ order_by_desc: bool = False
27
+ limit_count: int = -1
28
+
29
+ def where(self, query: Any) -> "Query":
30
+ """
31
+ Add a filter clause.
32
+ """
33
+ self.queries.append(query)
34
+ return self
35
+
36
+ def order_by(self, field: Any, desc: bool = False) -> "Query":
37
+ """
38
+ Order results by a field.
39
+ """
40
+ self.order_by_field = field
41
+ self.order_by_desc = desc
42
+ return self
43
+
44
+ def limit(self, limit: int) -> "Query":
45
+ """
46
+ Cap the number of rows returned. A non-positive value means no limit.
47
+ """
48
+ self.limit_count = limit
49
+ return self
50
+
51
+ def build(self) -> Any:
52
+ """
53
+ Produce the select statement.
54
+ """
55
+ statement = select(self.table)
56
+ for query in self.queries:
57
+ statement = statement.where(query)
58
+
59
+ if self.order_by_field is not None:
60
+ order_by_field = self.order_by_field
61
+ if self.order_by_desc:
62
+ order_by_field = _desc(order_by_field)
63
+ statement = statement.order_by(order_by_field)
64
+
65
+ if self.limit_count > 0:
66
+ statement = statement.limit(self.limit_count)
67
+
68
+ return statement
@@ -0,0 +1,96 @@
1
+ """
2
+ Base table type.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ from typing import Any
8
+
9
+ from sqlmodel import SQLModel
10
+
11
+ from corekit.registry import SmartRegistry
12
+
13
+ __all__ = ["NamedTable"]
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class NamedTable(SQLModel):
19
+ """
20
+ Base for tables that are addressable by a string id.
21
+
22
+ Subclasses register themselves by name, so a table class can be resolved
23
+ from a string -- useful for imports, exports and admin tooling that work
24
+ with table names rather than imported classes.
25
+ """
26
+
27
+ __registry__: SmartRegistry = SmartRegistry()
28
+
29
+ def __init_subclass__(cls, **kwargs: Any) -> None:
30
+ """
31
+ Register every subclass under its class name.
32
+ """
33
+ super().__init_subclass__(**kwargs)
34
+ NamedTable.__registry__[cls.__name__] = cls
35
+
36
+ @classmethod
37
+ def get_table_types(cls) -> SmartRegistry:
38
+ """
39
+ Return the registry of known table classes.
40
+ """
41
+ return cls.__registry__
42
+
43
+ @classmethod
44
+ def get_table_by_name(cls, name: str) -> type["NamedTable"] | None:
45
+ """
46
+ Look up a table class by name, using the registry's normalization.
47
+ """
48
+ return cls.__registry__.get(name)
49
+
50
+ @property
51
+ def table_name(self) -> str:
52
+ """
53
+ The name of the table this record belongs to.
54
+ """
55
+ return type(self).__name__
56
+
57
+ @classmethod
58
+ def get_display_name(cls) -> str:
59
+ """
60
+ A human-readable name for the table.
61
+ """
62
+ return getattr(cls, "__display_name__", None) or cls.__name__
63
+
64
+ @classmethod
65
+ def create_default(cls, identifier: str, **kwargs: Any) -> "NamedTable":
66
+ """
67
+ Build an instance carrying only its identifier and any supplied fields.
68
+ """
69
+ kwargs["id"] = kwargs.get("id") or identifier
70
+ logger.info(f"Creating default {cls.__name__} with id {kwargs['id']!r}")
71
+ return cls(**kwargs)
72
+
73
+ @staticmethod
74
+ def _serialize(value: Any) -> str:
75
+ return json.dumps(value)
76
+
77
+ @staticmethod
78
+ def _deserialize(value: str) -> Any:
79
+ try:
80
+ return json.loads(value)
81
+ except (TypeError, ValueError):
82
+ logger.error(f"Unable to deserialize value: {value!r}")
83
+ raise
84
+
85
+ def serialize(self, values: dict[str, Any], key: str) -> dict[str, Any]:
86
+ """
87
+ Replace ``values[key]`` with its JSON encoding.
88
+ """
89
+ values[key] = self._serialize(values.get(key))
90
+ return values
91
+
92
+ def deserialize(self, key: str) -> Any:
93
+ """
94
+ Decode the JSON stored in the named field.
95
+ """
96
+ return self._deserialize(getattr(self, key))
corekit/constants.py ADDED
@@ -0,0 +1,45 @@
1
+ # Minutes
2
+ ONE_MINUTE = 60
3
+ TWO_MINUTES = ONE_MINUTE * 2
4
+ THREE_MINUTES = ONE_MINUTE * 3
5
+ FIVE_MINUTES = ONE_MINUTE * 5
6
+ TEN_MINUTES = ONE_MINUTE * 10
7
+ FIFTEEN_MINUTES = ONE_MINUTE * 15
8
+ THIRTY_MINUTES = ONE_MINUTE * 30
9
+ FORTY_FIVE_MINUTES = ONE_MINUTE * 45
10
+
11
+ # Hours
12
+ ONE_HOUR = ONE_MINUTE * 60
13
+ TWO_HOURS = ONE_HOUR * 2
14
+ THREE_HOURS = ONE_HOUR * 3
15
+ FIVE_HOURS = ONE_HOUR * 5
16
+ SIX_HOURS = ONE_HOUR * 6
17
+ NINE_HOURS = ONE_HOUR * 9
18
+ TWELVE_HOURS = ONE_HOUR * 12
19
+ FIFTEEN_HOURS = ONE_HOUR * 15
20
+ EIGHTEEN_HOURS = ONE_HOUR * 18
21
+
22
+ # Days
23
+ ONE_DAY = ONE_HOUR * 24
24
+ TWO_DAYS = ONE_DAY * 2
25
+ THREE_DAYS = ONE_DAY * 3
26
+ FOUR_DAYS = ONE_DAY * 4
27
+ FIVE_DAYS = ONE_DAY * 5
28
+ SIX_DAYS = ONE_DAY * 6
29
+ TEN_DAYS = ONE_DAY * 10
30
+ FIFTEEN_DAYS = ONE_DAY * 15
31
+ THIRTY_DAYS = ONE_DAY * 30
32
+ FORTY_FIVE_DAYS = ONE_DAY * 45
33
+ SIXTY_FIVE_DAYS = ONE_DAY * 60
34
+ NINETY_FIVE_DAYS = ONE_DAY * 90
35
+ ONE_HUNDRED_TWENTY_DAYS = ONE_DAY * 120
36
+ ONE_HUNDRED_EIGHTY_DAYS = ONE_DAY * 180
37
+
38
+ # Weeks
39
+ ONE_WEEK = ONE_DAY * 7
40
+ TWO_WEEKS = ONE_WEEK * 2
41
+ THREE_WEEKS = ONE_WEEK * 3
42
+ FOUR_WEEKS = ONE_WEEK * 4
43
+
44
+ # Years
45
+ ONE_YEAR = ONE_DAY * 365
@@ -0,0 +1 @@
1
+ from .hasher import Hasher
@@ -0,0 +1,7 @@
1
+ HASH_JOINER = ":"
2
+ EMPTY_HASH = ""
3
+
4
+ # NOTE: a hardcoded CRYPTO_SALT previously lived here and is still present in this
5
+ # repository's git history. It must be considered compromised and must never be
6
+ # reintroduced. The salt is now supplied through configuration -- see
7
+ # `corekit.config.CorekitSettings.crypto_salt` and `corekit.crypto.hasher.Hasher`.
corekit/crypto/enum.py ADDED
@@ -0,0 +1,11 @@
1
+ from corekit.schemas.enum import StringEnum
2
+
3
+
4
+ class SaltMethod(StringEnum):
5
+ PRE = "pre"
6
+ POST = "post"
7
+ BOTH = "BOTH"
8
+
9
+ @classmethod
10
+ def get_default(cls) -> "SaltMethod":
11
+ return cls.BOTH
@@ -0,0 +1,89 @@
1
+ import hashlib
2
+ from typing import Any
3
+
4
+ from corekit.config import get_settings
5
+ from corekit.crypto.constants import EMPTY_HASH, HASH_JOINER
6
+ from corekit.crypto.enum import SaltMethod
7
+ from corekit.observability.loggable import Loggable
8
+
9
+
10
+ class MissingSaltError(RuntimeError):
11
+ """
12
+ Raised when hashing is attempted without a configured salt.
13
+ """
14
+
15
+
16
+ class Hasher(Loggable):
17
+ """
18
+ Salted SHA-256 hashing.
19
+
20
+ The salt has no default. Supply it explicitly, or configure it via
21
+ ``COREKIT_CRYPTO_SALT`` / ``crypto_salt`` in a corekit config file. Resolution
22
+ is deferred until first use so that importing this module never fails.
23
+ """
24
+
25
+ def __init__(
26
+ self, joiner: str = HASH_JOINER, salt: str | None = None, salt_method: SaltMethod = SaltMethod.get_default()
27
+ ) -> None:
28
+ super().__init__()
29
+ self._joiner = joiner
30
+ self._explicit_salt = salt
31
+ self._salt_method = salt_method
32
+
33
+ @property
34
+ def _salt(self) -> str:
35
+ """
36
+ Resolve the salt, preferring the explicit argument over configuration.
37
+ """
38
+ salt = self._explicit_salt
39
+ if salt is None:
40
+ salt = get_settings().crypto.salt
41
+ if not salt:
42
+ raise MissingSaltError(
43
+ "No crypto salt configured. Pass salt=... to Hasher, set "
44
+ "COREKIT_CRYPTO_SALT, or add crypto_salt to your corekit config. "
45
+ 'Generate one with: python -c "import secrets; print(secrets.token_hex(32))"'
46
+ )
47
+ return salt
48
+
49
+ def salt(self, *args: Any) -> str:
50
+ """
51
+ Given any number of inputs, combine and salt the input in preparation for hashing.
52
+ """
53
+ if len(args) < 1:
54
+ self.warning("No input to hash. Returning empty hash")
55
+ return EMPTY_HASH
56
+
57
+ salted: str = ""
58
+ # Add the prefix for all methods other than POST
59
+ if self._salt_method != SaltMethod.POST:
60
+ salted += f"{self._salt}{self._joiner}"
61
+
62
+ salted += self._joiner.join(str(arg) for arg in args)
63
+
64
+ # Add the suffix for all methods other than PRE
65
+ if self._salt_method != SaltMethod.PRE:
66
+ salted += f"{self._joiner}{self._salt}"
67
+
68
+ return salted
69
+
70
+ def hash(self, *args: Any) -> str:
71
+ """
72
+ Salt the inputs and return their SHA-256 digest.
73
+
74
+ With no input this returns EMPTY_HASH rather than the digest of the
75
+ empty string, so "nothing was hashed" stays distinguishable from a real
76
+ digest -- which is what salt() already reports and what EMPTY_HASH is
77
+ for.
78
+ """
79
+ to_hash = self.salt(*args)
80
+ if to_hash == EMPTY_HASH:
81
+ return EMPTY_HASH
82
+ return hashlib.sha256(to_hash.encode(self.encoding)).hexdigest()
83
+
84
+ @property
85
+ def encoding(self) -> str:
86
+ """
87
+ The encoding used to turn salted text into bytes.
88
+ """
89
+ return "utf-8"
@@ -0,0 +1,81 @@
1
+ """
2
+ In-memory datasets with composable filtering.
3
+
4
+ ``Dataset`` holds ``__slots__``-based records sharing one schema, with O(1)
5
+ lookup by id, in-place editing, schema evolution and clean pickling. Filtering
6
+ uses expressions rather than callables::
7
+
8
+ from corekit.data import Dataset, Field
9
+
10
+ people = Dataset(id_key="name", schema=["name", "age"])
11
+ people.add({"name": "Ada", "age": 36})
12
+
13
+ adults = people.filter(Field("age") >= 18)
14
+ people.get_record("Ada").age
15
+
16
+ ``Field`` is an alias of ``FieldExpression``: ``Field("a") > Field("b")``
17
+ compares two fields, while ``Field("a") > 3`` compares against a value.
18
+
19
+ Depends only on the standard library.
20
+ """
21
+
22
+ from corekit.data.dataset import Dataset
23
+ from corekit.data.expressions import (
24
+ And,
25
+ Comparison,
26
+ Contains,
27
+ Equals,
28
+ Expression,
29
+ FieldExpression,
30
+ GreaterThan,
31
+ GreaterThanOrEquals,
32
+ IsIn,
33
+ LessThan,
34
+ LessThanOrEquals,
35
+ Not,
36
+ NotEquals,
37
+ Or,
38
+ ValueExpression,
39
+ )
40
+ from corekit.data.record import BaseRecord, is_valid_key, resolve_default
41
+ from corekit.data.stats import (
42
+ CategoricalFieldStats,
43
+ DatasetStats,
44
+ FieldDescription,
45
+ FieldStats,
46
+ NumericFieldStats,
47
+ ValueCounts,
48
+ )
49
+
50
+ # The expression used to name a field. Field("age") reads better than
51
+ # FieldExpression("age") at a call site.
52
+ Field = FieldExpression
53
+
54
+ __all__ = [
55
+ "And",
56
+ "BaseRecord",
57
+ "CategoricalFieldStats",
58
+ "Comparison",
59
+ "Contains",
60
+ "Dataset",
61
+ "DatasetStats",
62
+ "Equals",
63
+ "Expression",
64
+ "Field",
65
+ "FieldDescription",
66
+ "FieldExpression",
67
+ "FieldStats",
68
+ "GreaterThan",
69
+ "GreaterThanOrEquals",
70
+ "IsIn",
71
+ "LessThan",
72
+ "LessThanOrEquals",
73
+ "Not",
74
+ "NotEquals",
75
+ "NumericFieldStats",
76
+ "Or",
77
+ "ValueCounts",
78
+ "ValueExpression",
79
+ "is_valid_key",
80
+ "resolve_default",
81
+ ]