clm-kernel 0.0.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.
- clm/__init__.py +230 -0
- clm/__main__.py +25 -0
- clm/cross_cutting/__init__.py +50 -0
- clm/cross_cutting/_test_runner.py +36 -0
- clm/cross_cutting/async_utils.py +25 -0
- clm/cross_cutting/cel_builder.py +123 -0
- clm/cross_cutting/cel_converter.py +697 -0
- clm/cross_cutting/cel_engine.py +252 -0
- clm/cross_cutting/cel_ops.py +72 -0
- clm/cross_cutting/cel_ops_db.py +157 -0
- clm/cross_cutting/cel_ops_files.py +99 -0
- clm/cross_cutting/cel_ops_state.py +71 -0
- clm/cross_cutting/cel_ops_strings.py +64 -0
- clm/cross_cutting/cel_resolver.py +63 -0
- clm/cross_cutting/cel_slices.py +56 -0
- clm/cross_cutting/cel_store.py +330 -0
- clm/cross_cutting/clm_logger.py +140 -0
- clm/cross_cutting/config/__init__.py +155 -0
- clm/cross_cutting/config/config_constants.py +392 -0
- clm/cross_cutting/config/env_parameters.py +90 -0
- clm/cross_cutting/config/logging.py +378 -0
- clm/cross_cutting/config/settings.py +504 -0
- clm/cross_cutting/context.py +5 -0
- clm/cross_cutting/domain_models.py +55 -0
- clm/cross_cutting/domain_types.py +61 -0
- clm/cross_cutting/effects.py +67 -0
- clm/cross_cutting/errors.py +153 -0
- clm/cross_cutting/governance.py +331 -0
- clm/cross_cutting/guards.py +41 -0
- clm/cross_cutting/mcard.py +321 -0
- clm/cross_cutting/native.py +185 -0
- clm/cross_cutting/observability.py +289 -0
- clm/cross_cutting/pcard.py +434 -0
- clm/cross_cutting/registry.py +71 -0
- clm/cross_cutting/resources.py +84 -0
- clm/cross_cutting/secrets.py +44 -0
- clm/cross_cutting/subprocess_utils.py +118 -0
- clm/cross_cutting/telemetry.py +164 -0
- clm/cross_cutting/testing/__init__.py +8 -0
- clm/cross_cutting/testing/comparator.py +187 -0
- clm/cross_cutting/testing/datasets.py +87 -0
- clm/cross_cutting/timeutil.py +20 -0
- clm/cross_cutting/types/new_type_check.py +0 -0
- clm/cross_cutting/utils/__init__.py +1 -0
- clm/cross_cutting/utils/url_safety.py +50 -0
- clm/cross_cutting/vcard.py +560 -0
- clm/cross_cutting/writer.py +118 -0
- clm/layer0/README.md +50 -0
- clm/layer0/__init__.py +223 -0
- clm/layer0/algebra.py +60 -0
- clm/layer0/algebra_baldwin.py +133 -0
- clm/layer0/algebra_baldwin_generators.py +36 -0
- clm/layer0/algebra_equivalence.py +262 -0
- clm/layer0/algebra_ingest.py +306 -0
- clm/layer0/algebra_manifest_io.py +156 -0
- clm/layer0/assembler.py +481 -0
- clm/layer0/cel/__init__.py +23 -0
- clm/layer0/cel_eval.py +491 -0
- clm/layer0/codec.py +106 -0
- clm/layer0/context.py +122 -0
- clm/layer0/db.py +160 -0
- clm/layer0/exceptions.py +73 -0
- clm/layer0/fibration.py +106 -0
- clm/layer0/gates.py +50 -0
- clm/layer0/hash.py +80 -0
- clm/layer0/loader.py +826 -0
- clm/layer0/math_utils.py +163 -0
- clm/layer0/mcard.py +96 -0
- clm/layer0/model/__init__.py +0 -0
- clm/layer0/model/action.py +223 -0
- clm/layer0/model/card.py +280 -0
- clm/layer0/model/card_triad.py +427 -0
- clm/layer0/model/compatibility.py +60 -0
- clm/layer0/model/dictionary.py +46 -0
- clm/layer0/model/dots.py +331 -0
- clm/layer0/model/event_producer.py +88 -0
- clm/layer0/model/g_time.py +89 -0
- clm/layer0/model/handle.py +163 -0
- clm/layer0/model/hash/__init__.py +6 -0
- clm/layer0/model/hash/algorithms/__init__.py +3 -0
- clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
- clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
- clm/layer0/model/hash/constants.py +53 -0
- clm/layer0/model/hash/enums.py +60 -0
- clm/layer0/model/hash/validator.py +307 -0
- clm/layer0/model/interpreter.py +275 -0
- clm/layer0/model/pagination.py +45 -0
- clm/layer0/model/pcard.py +716 -0
- clm/layer0/model/schema.py +44 -0
- clm/layer0/model/utils/__init__.py +1 -0
- clm/layer0/model/utils/content_analyzer.py +152 -0
- clm/layer0/model/validators/__init__.py +1 -0
- clm/layer0/model/validators/base_validator.py +44 -0
- clm/layer0/model/validators/binary_validator.py +66 -0
- clm/layer0/model/validators/text_validator.py +82 -0
- clm/layer0/model/validators/validation_registry.py +49 -0
- clm/layer0/model/vcard.py +823 -0
- clm/layer0/model/vcard_ext/__init__.py +39 -0
- clm/layer0/model/vcard_ext/core.py +60 -0
- clm/layer0/model/vcard_ext/network.py +23 -0
- clm/layer0/model/vcard_ext/observability.py +63 -0
- clm/layer0/model/vcard_ext/storage.py +60 -0
- clm/layer0/model/vcard_ext/vendors.py +211 -0
- clm/layer0/model/vcard_sandwich.py +119 -0
- clm/layer0/model/vcard_vocabulary.py +463 -0
- clm/layer0/model/workflow.py +274 -0
- clm/layer0/narrative.py +431 -0
- clm/layer0/ontology/collection.py +125 -0
- clm/layer0/parser.py +429 -0
- clm/layer0/relations.py +73 -0
- clm/layer0/rule_evaluator.py +251 -0
- clm/layer0/schema/__init__.py +38 -0
- clm/layer0/schema/ddl.py +97 -0
- clm/layer0/templates/abstract.yaml +99 -0
- clm/layer0/templates/balanced.yaml +253 -0
- clm/layer0/templates/concrete.yaml +172 -0
- clm/layer0/type_registry.py +223 -0
- clm/layer0/types.py +204 -0
- clm/layer0/utils.py +886 -0
- clm/layer0/verifier_core.py +262 -0
- clm/layer1/__init__.py +31 -0
- clm/layer1/action_dispatcher.py +275 -0
- clm/layer1/adapter_registry.py +233 -0
- clm/layer1/algebra_baldwin_generators.py +192 -0
- clm/layer1/arrow_compiler.py +525 -0
- clm/layer1/batch_processor.py +152 -0
- clm/layer1/cel/__init__.py +27 -0
- clm/layer1/cel/builder.py +173 -0
- clm/layer1/engine.py +759 -0
- clm/layer1/engine_async.py +89 -0
- clm/layer1/eval_callback.py +49 -0
- clm/layer1/gate_composer.py +229 -0
- clm/layer1/hoare_sandwich.py +298 -0
- clm/layer1/identity_guard.py +348 -0
- clm/layer1/matrix.py +138 -0
- clm/layer1/neural_runtime.py +221 -0
- clm/layer1/operations/__init__.py +123 -0
- clm/layer1/operations/augment.py +156 -0
- clm/layer1/operations/base.py +109 -0
- clm/layer1/operations/builtins.py +338 -0
- clm/layer1/operations/dispatch.py +144 -0
- clm/layer1/operations/exclude.py +107 -0
- clm/layer1/operations/handle.py +197 -0
- clm/layer1/operations/identity.py +139 -0
- clm/layer1/operations/invert.py +66 -0
- clm/layer1/operations/loader.py +116 -0
- clm/layer1/operations/manipulators/__init__.py +23 -0
- clm/layer1/operations/manipulators/document.py +211 -0
- clm/layer1/operations/manipulators/media.py +134 -0
- clm/layer1/operations/manipulators/structured.py +159 -0
- clm/layer1/operations/manipulators/tabular.py +334 -0
- clm/layer1/operations/port.py +65 -0
- clm/layer1/operations/services.py +529 -0
- clm/layer1/operations/split.py +117 -0
- clm/layer1/operations/substitute.py +75 -0
- clm/layer1/petri.py +1052 -0
- clm/layer1/pocketflow_orchestrator.py +408 -0
- clm/layer1/pocketflow_scheduler.py +519 -0
- clm/layer1/protocols.py +197 -0
- clm/layer1/runtime_adapter.py +718 -0
- clm/layer1/runtimes/__init__.py +71 -0
- clm/layer1/runtimes/_generated_assertions.py +125 -0
- clm/layer1/runtimes/_generated_types.py +42 -0
- clm/layer1/runtimes/base.py +265 -0
- clm/layer1/runtimes/binary.py +101 -0
- clm/layer1/runtimes/factory.py +203 -0
- clm/layer1/runtimes/javascript.py +321 -0
- clm/layer1/runtimes/javascript_runtime.js +33 -0
- clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
- clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
- clm/layer1/runtimes/lambda_calc.py +126 -0
- clm/layer1/runtimes/python.py +288 -0
- clm/layer1/runtimes/python_runtime.py +41 -0
- clm/layer1/runtimes/script.py +114 -0
- clm/layer1/sandbox.py +334 -0
- clm/layer1/savepoint_guard.py +136 -0
- clm/layer1/sparse_net.py +122 -0
- clm/layer1/spec_tester.py +165 -0
- clm/layer1/store.py +295 -0
- clm/layer1/test_runner.py +498 -0
- clm/layer1/type_inspector.py +166 -0
- clm/layer1/verifier.py +772 -0
- clm/layer1/vm.py +91 -0
- clm/layer2/__init__.py +78 -0
- clm/layer2/card_collection.py +678 -0
- clm/layer2/classifier.py +479 -0
- clm/layer2/engine/__init__.py +1 -0
- clm/layer2/engine/abstract_sql_engine.py +320 -0
- clm/layer2/engine/base.py +193 -0
- clm/layer2/engine/duckdb_engine.py +576 -0
- clm/layer2/engine/sqlite_engine.py +518 -0
- clm/layer2/fiber/__init__.py +31 -0
- clm/layer2/fiber/context.py +89 -0
- clm/layer2/fiber/disposable.py +163 -0
- clm/layer2/fiber/fiber.py +125 -0
- clm/layer2/fiber/hoare.py +191 -0
- clm/layer2/file_io.py +376 -0
- clm/layer2/file_registrar.py +77 -0
- clm/layer2/ingest.py +202 -0
- clm/layer2/lineage.py +322 -0
- clm/layer2/mcard_fs.py +262 -0
- clm/layer2/merkle.py +11 -0
- clm/layer2/mime.py +244 -0
- clm/layer2/mime_detector.py +245 -0
- clm/layer2/private_collection.py +118 -0
- clm/layer2/shared_store.py +11 -0
- clm/layer2/storage/__init__.py +190 -0
- clm/layer2/storage/db_bitemporal.py +141 -0
- clm/layer2/storage/db_driver.py +331 -0
- clm/layer2/storage/db_hyperlinks.py +214 -0
- clm/layer2/storage/db_pool.py +139 -0
- clm/layer2/storage/db_repository.py +229 -0
- clm/layer2/storage/savepoint.py +78 -0
- clm/layer2/storage/sqlite_wal.py +74 -0
- clm/layer2/tridb.py +245 -0
- clm/layer2/vcard_receipt.py +71 -0
- clm/layer2/vfs.py +17 -0
- clm/layer3/__init__.py +141 -0
- clm/layer3/agency/__init__.py +19 -0
- clm/layer3/agency/config.py +220 -0
- clm/layer3/agency/providers/__init__.py +15 -0
- clm/layer3/agency/providers/base.py +91 -0
- clm/layer3/agency/providers/mlc_llm.py +169 -0
- clm/layer3/agency/providers/ollama.py +203 -0
- clm/layer3/agency/router.py +71 -0
- clm/layer3/agency/runtime.py +524 -0
- clm/layer3/gateway.py +353 -0
- clm/layer3/open_interpreter_adapter.py +159 -0
- clm/layer3/rag/__init__.py +70 -0
- clm/layer3/rag/cli.py +228 -0
- clm/layer3/rag/config.py +153 -0
- clm/layer3/rag/embeddings/__init__.py +16 -0
- clm/layer3/rag/embeddings/base.py +79 -0
- clm/layer3/rag/embeddings/ollama.py +211 -0
- clm/layer3/rag/embeddings/vision.py +290 -0
- clm/layer3/rag/engine.py +276 -0
- clm/layer3/rag/graph/__init__.py +22 -0
- clm/layer3/rag/graph/community.py +198 -0
- clm/layer3/rag/graph/engine.py +433 -0
- clm/layer3/rag/graph/extractor.py +255 -0
- clm/layer3/rag/graph/schema.py +115 -0
- clm/layer3/rag/graph/store.py +641 -0
- clm/layer3/rag/indexer.py +333 -0
- clm/layer3/rag/llm_providers.json +52 -0
- clm/layer3/rag/semantic_versioning.py +313 -0
- clm/layer3/rag/vector/__init__.py +28 -0
- clm/layer3/rag/vector/handle_vector_store.py +749 -0
- clm/layer3/rag/vector/schema.py +151 -0
- clm/layer3/rag/vector/store.py +632 -0
- clm/layer3/reticulum/__init__.py +55 -0
- clm/layer3/reticulum/bridge.py +74 -0
- clm/layer3/reticulum/daemon.py +88 -0
- clm/layer3/reticulum/discovery.py +164 -0
- clm/layer3/reticulum/identity.py +107 -0
- clm/layer3/reticulum/media.py +148 -0
- clm/layer3/reticulum/transport.py +133 -0
- clm/layer3/satori/__init__.py +209 -0
- clm/layer3/satori/alpha_conversion.py +168 -0
- clm/layer3/satori/beta_reduction.py +359 -0
- clm/layer3/satori/continuation.py +38 -0
- clm/layer3/satori/eta_conversion.py +205 -0
- clm/layer3/satori/free_variables.py +163 -0
- clm/layer3/satori/io_effects.py +370 -0
- clm/layer3/satori/lambda_runtime.py +726 -0
- clm/layer3/satori/lambda_term.py +230 -0
- clm/layer3/satori/sheaf_audit.py +261 -0
- clm/layer3/satori/speech_act.py +279 -0
- clm/layer4/README.md +9 -0
- clm/layer4/__init__.py +67 -0
- clm/layer4/_sovereign_executor.py +580 -0
- clm/layer4/action_commands.py +719 -0
- clm/layer4/api.py +236 -0
- clm/layer4/bootstrap/__init__.py +13 -0
- clm/layer4/bootstrap/bootstrapper.py +118 -0
- clm/layer4/bootstrap/cleanup.py +44 -0
- clm/layer4/bootstrap/clm_hooks.py +49 -0
- clm/layer4/bootstrap/genesis.py +77 -0
- clm/layer4/bootstrap/injection.py +125 -0
- clm/layer4/cli.py +549 -0
- clm/layer4/collection_manager.py +553 -0
- clm/layer4/commands/__init__.py +81 -0
- clm/layer4/commands/action_cmd.py +42 -0
- clm/layer4/commands/check_cmd.py +82 -0
- clm/layer4/commands/envelope.py +73 -0
- clm/layer4/commands/evaluate_cmd.py +224 -0
- clm/layer4/commands/mcard_cmd.py +24 -0
- clm/layer4/commands/petri_cmd.py +92 -0
- clm/layer4/commands/tridb_cmd.py +319 -0
- clm/layer4/commands/version_cmd.py +27 -0
- clm/layer4/diff_commands.py +167 -0
- clm/layer4/eoa_sponsor.py +232 -0
- clm/layer4/events.py +69 -0
- clm/layer4/flux_gateway.py +133 -0
- clm/layer4/gateway.py +91 -0
- clm/layer4/loader.py +307 -0
- clm/layer4/mcard_commands.py +166 -0
- clm/layer4/membrane.py +273 -0
- clm/layer4/navigation.py +50 -0
- clm/layer4/protocol/protocol_entry_python.py +108 -0
- clm/layer4/protocol/python_sync.py +126 -0
- clm/layer4/runner.py +150 -0
- clm/layer4/satori.py +262 -0
- clm/layer4/server/python_sync_client.py +150 -0
- clm/layer4/server/python_sync_server.py +139 -0
- clm/layer4/server/service_api.py +315 -0
- clm/layer4/server/start_servers.py +107 -0
- clm/layer4/server/stop_servers.py +24 -0
- clm/layer4/simulated_optimization_fix.py +0 -0
- clm/layer4/storage.py +463 -0
- clm/layer4/tridb_admin.py +268 -0
- clm/layer4/websocket/test_ws_client.py +104 -0
- clm/layer4/websocket/test_ws_server.py +138 -0
- clm/layer4/websocket/ws_server_local.py +160 -0
- clm/layer5/__init__.py +51 -0
- clm/layer5/async_batch_writer.py +133 -0
- clm/layer5/learning.py +276 -0
- clm/layer5/telemetry_hook.py +107 -0
- clm/layer5/type_lattice.py +238 -0
- clm/py.typed +0 -0
- clm/schemas/__init__.py +652 -0
- clm/schemas/abstract_face_schema.yaml +77 -0
- clm/schemas/cel_schema.yaml +416 -0
- clm/schemas/lifecycle/__init__.py +45 -0
- clm/schemas/lifecycle/models.py +123 -0
- clm/schemas/lifecycle/service.py +364 -0
- clm/schemas/mcard_schema.sql +192 -0
- clm/schemas/mcard_vector_schema.sql +283 -0
- clm/schemas/mlp-context-v1.json +91 -0
- clm_kernel-0.0.0.dist-info/METADATA +121 -0
- clm_kernel-0.0.0.dist-info/RECORD +333 -0
- clm_kernel-0.0.0.dist-info/WHEEL +4 -0
- clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
- clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import struct
|
|
5
|
+
|
|
6
|
+
from clm.layer0.model.hash.enums import HashAlgorithm
|
|
7
|
+
|
|
8
|
+
# Set up basic logging for the module
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
logger.setLevel(logging.DEBUG) # Set to DEBUG to capture detailed messages
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ── Zero-dependency pure Python BLAKE3 engine (INV-02 fallback) ─────────
|
|
14
|
+
_B3_IV = (
|
|
15
|
+
0x6A09E667,
|
|
16
|
+
0xBB67AE85,
|
|
17
|
+
0x3C6EF372,
|
|
18
|
+
0xA54FF53A,
|
|
19
|
+
0x510E527F,
|
|
20
|
+
0x9B05688C,
|
|
21
|
+
0x1F83D9AB,
|
|
22
|
+
0x5BE0CD19,
|
|
23
|
+
)
|
|
24
|
+
_B3_MSG_PERM = (2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8)
|
|
25
|
+
_B3_CHUNK_START, _B3_CHUNK_END, _B3_PARENT, _B3_ROOT = 1, 2, 4, 8
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _b3_rotr(w: int, r: int) -> int:
|
|
29
|
+
return ((w >> r) | (w << (32 - r))) & 0xFFFFFFFF
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _b3_g(s: list, a: int, b: int, c: int, d: int, mx: int, my: int) -> None:
|
|
33
|
+
s[a] = (s[a] + s[b] + mx) & 0xFFFFFFFF
|
|
34
|
+
s[d] = _b3_rotr(s[d] ^ s[a], 16)
|
|
35
|
+
s[c] = (s[c] + s[d]) & 0xFFFFFFFF
|
|
36
|
+
s[b] = _b3_rotr(s[b] ^ s[c], 12)
|
|
37
|
+
s[a] = (s[a] + s[b] + my) & 0xFFFFFFFF
|
|
38
|
+
s[d] = _b3_rotr(s[d] ^ s[a], 8)
|
|
39
|
+
s[c] = (s[c] + s[d]) & 0xFFFFFFFF
|
|
40
|
+
s[b] = _b3_rotr(s[b] ^ s[c], 7)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _b3_round(s: list, m: list) -> None:
|
|
44
|
+
_b3_g(s, 0, 4, 8, 12, m[0], m[1])
|
|
45
|
+
_b3_g(s, 1, 5, 9, 13, m[2], m[3])
|
|
46
|
+
_b3_g(s, 2, 6, 10, 14, m[4], m[5])
|
|
47
|
+
_b3_g(s, 3, 7, 11, 15, m[6], m[7])
|
|
48
|
+
_b3_g(s, 0, 5, 10, 15, m[8], m[9])
|
|
49
|
+
_b3_g(s, 1, 6, 11, 12, m[10], m[11])
|
|
50
|
+
_b3_g(s, 2, 7, 8, 13, m[12], m[13])
|
|
51
|
+
_b3_g(s, 3, 4, 9, 14, m[14], m[15])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _b3_compress(
|
|
55
|
+
cv: list, block_words: list, block_len: int, counter: int, flags: int
|
|
56
|
+
) -> list:
|
|
57
|
+
s = (
|
|
58
|
+
list(cv)
|
|
59
|
+
+ list(_B3_IV[:4])
|
|
60
|
+
+ [counter & 0xFFFFFFFF, (counter >> 32) & 0xFFFFFFFF, block_len, flags]
|
|
61
|
+
)
|
|
62
|
+
m = list(block_words)
|
|
63
|
+
for r in range(7):
|
|
64
|
+
_b3_round(s, m)
|
|
65
|
+
if r < 6:
|
|
66
|
+
m = [m[_B3_MSG_PERM[i]] for i in range(16)]
|
|
67
|
+
out = [0] * 16
|
|
68
|
+
for i in range(8):
|
|
69
|
+
out[i] = (s[i] ^ s[i + 8]) & 0xFFFFFFFF
|
|
70
|
+
out[i + 8] = (s[i + 8] ^ cv[i]) & 0xFFFFFFFF
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _b3_words_from_bytes(b: bytearray) -> list:
|
|
75
|
+
words = [0] * 16
|
|
76
|
+
for i in range(min(16, (len(b) + 3) // 4)):
|
|
77
|
+
chunk = b[i * 4 : (i + 1) * 4]
|
|
78
|
+
words[i] = int.from_bytes(chunk, "little")
|
|
79
|
+
return words
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _b3_words_to_hex(words: list) -> str:
|
|
83
|
+
return "".join(struct.pack("<I", w).hex() for w in words)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _B3ChunkState:
|
|
87
|
+
def __init__(self, counter: int, flags: int = 0):
|
|
88
|
+
self.cv = list(_B3_IV)
|
|
89
|
+
self.counter = counter
|
|
90
|
+
self.flags = flags
|
|
91
|
+
self.block = bytearray()
|
|
92
|
+
self.blocks_compressed = 0
|
|
93
|
+
|
|
94
|
+
def start_flag(self) -> int:
|
|
95
|
+
return _B3_CHUNK_START if self.blocks_compressed == 0 else 0
|
|
96
|
+
|
|
97
|
+
def update(self, data: bytes) -> None:
|
|
98
|
+
offset = 0
|
|
99
|
+
while offset < len(data):
|
|
100
|
+
if len(self.block) == 64:
|
|
101
|
+
words = _b3_words_from_bytes(self.block)
|
|
102
|
+
out = _b3_compress(
|
|
103
|
+
self.cv, words, 64, self.counter, self.flags | self.start_flag()
|
|
104
|
+
)
|
|
105
|
+
self.cv = out[:8]
|
|
106
|
+
self.blocks_compressed += 1
|
|
107
|
+
self.block = bytearray()
|
|
108
|
+
take = min(64 - len(self.block), len(data) - offset)
|
|
109
|
+
self.block.extend(data[offset : offset + take])
|
|
110
|
+
offset += take
|
|
111
|
+
|
|
112
|
+
def output(self, is_root: bool = False) -> list:
|
|
113
|
+
words = _b3_words_from_bytes(self.block)
|
|
114
|
+
flags = self.flags | self.start_flag() | _B3_CHUNK_END
|
|
115
|
+
if is_root:
|
|
116
|
+
flags |= _B3_ROOT
|
|
117
|
+
return _b3_compress(self.cv, words, len(self.block), self.counter, flags)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _b3_parent_output(left_cv: list, right_cv: list, is_root: bool = False) -> list:
|
|
121
|
+
block_words = left_cv[:8] + right_cv[:8]
|
|
122
|
+
flags = _B3_PARENT
|
|
123
|
+
if is_root:
|
|
124
|
+
flags |= _B3_ROOT
|
|
125
|
+
return _b3_compress(_B3_IV, block_words, 64, 0, flags)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def pure_python_blake3(data: bytes) -> str:
|
|
129
|
+
"""Zero-dependency pure Python BLAKE3 hash computation."""
|
|
130
|
+
if isinstance(data, str):
|
|
131
|
+
data = data.encode("utf-8")
|
|
132
|
+
if len(data) <= 1024:
|
|
133
|
+
c = _B3ChunkState(0, 0)
|
|
134
|
+
c.update(data)
|
|
135
|
+
return _b3_words_to_hex(c.output(True)[:8])
|
|
136
|
+
|
|
137
|
+
stack = []
|
|
138
|
+
chunk_counter = 0
|
|
139
|
+
offset = 0
|
|
140
|
+
while offset + 1024 < len(data):
|
|
141
|
+
chunk_bytes = data[offset : offset + 1024]
|
|
142
|
+
c = _B3ChunkState(chunk_counter, 0)
|
|
143
|
+
c.update(chunk_bytes)
|
|
144
|
+
chunk_cv = c.output(False)[:8]
|
|
145
|
+
chunk_counter += 1
|
|
146
|
+
offset += 1024
|
|
147
|
+
|
|
148
|
+
total = chunk_counter
|
|
149
|
+
while (total & 1) == 0:
|
|
150
|
+
left_cv = stack.pop()
|
|
151
|
+
chunk_cv = _b3_parent_output(left_cv, chunk_cv, False)[:8]
|
|
152
|
+
total >>= 1
|
|
153
|
+
stack.append(chunk_cv)
|
|
154
|
+
|
|
155
|
+
final = _B3ChunkState(chunk_counter, 0)
|
|
156
|
+
final.update(data[offset:])
|
|
157
|
+
out = final.output(len(stack) == 0)[:8]
|
|
158
|
+
if len(stack) == 0:
|
|
159
|
+
return _b3_words_to_hex(out)
|
|
160
|
+
|
|
161
|
+
while stack:
|
|
162
|
+
left = stack.pop()
|
|
163
|
+
is_root = len(stack) == 0
|
|
164
|
+
out = _b3_parent_output(left, out, is_root)[:8]
|
|
165
|
+
return _b3_words_to_hex(out)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class HashValidator:
|
|
169
|
+
"""Validates and computes hash values for content."""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
content: str | bytes,
|
|
174
|
+
hash_algorithm: str | HashAlgorithm = HashAlgorithm.DEFAULT,
|
|
175
|
+
):
|
|
176
|
+
self.content = content
|
|
177
|
+
self.hash_algorithm = hash_algorithm
|
|
178
|
+
self.hash_value = self.compute_hash(content, hash_algorithm)
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def compute_hash(
|
|
182
|
+
content: str | bytes,
|
|
183
|
+
hash_algorithm: str | HashAlgorithm = HashAlgorithm.DEFAULT,
|
|
184
|
+
) -> str:
|
|
185
|
+
"""Compute hash for given content using specified hash function"""
|
|
186
|
+
content_to_hash = (
|
|
187
|
+
content if isinstance(content, bytes) else content.encode("utf-8")
|
|
188
|
+
) # Explicit encoding
|
|
189
|
+
|
|
190
|
+
# Handle HashAlgorithm conversion
|
|
191
|
+
if isinstance(hash_algorithm, str):
|
|
192
|
+
try:
|
|
193
|
+
hash_algorithm = HashAlgorithm(hash_algorithm.lower())
|
|
194
|
+
except ValueError as e:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"'{hash_algorithm}' is not a valid HashAlgorithm"
|
|
197
|
+
) from e
|
|
198
|
+
|
|
199
|
+
if hash_algorithm == HashAlgorithm.CUSTOM:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
"Custom hash function must be provided when using CUSTOM algorithm"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if hash_algorithm == HashAlgorithm.BLAKE3:
|
|
205
|
+
try:
|
|
206
|
+
import blake3
|
|
207
|
+
|
|
208
|
+
return blake3.blake3(content_to_hash).hexdigest()
|
|
209
|
+
except ImportError:
|
|
210
|
+
logger.warning(
|
|
211
|
+
"Py-blake3 not found. Attempting OS fallback for BLAKE3."
|
|
212
|
+
)
|
|
213
|
+
candidates = [
|
|
214
|
+
os.environ.get("CLM_RUST_CORE_BIN"),
|
|
215
|
+
os.path.join(
|
|
216
|
+
os.path.dirname(__file__),
|
|
217
|
+
"../../../clm/kernel/clm_rust_core/target/release/clm_rust_core",
|
|
218
|
+
),
|
|
219
|
+
os.path.join(
|
|
220
|
+
os.path.dirname(__file__),
|
|
221
|
+
"../../../clm/kernel/clm_rust_core/target/debug/clm_rust_core",
|
|
222
|
+
),
|
|
223
|
+
"clm_rust_core",
|
|
224
|
+
"b3sum",
|
|
225
|
+
]
|
|
226
|
+
if os.environ.get("CLM_ZERO_FS", "").lower() == "true":
|
|
227
|
+
return pure_python_blake3(content_to_hash)
|
|
228
|
+
|
|
229
|
+
for bin_path in candidates:
|
|
230
|
+
if not bin_path:
|
|
231
|
+
continue
|
|
232
|
+
try:
|
|
233
|
+
import subprocess
|
|
234
|
+
# Assuming standard CLI argument structure for the tool
|
|
235
|
+
cmd = (
|
|
236
|
+
[bin_path, "hash"]
|
|
237
|
+
if "clm_rust_core" in bin_path
|
|
238
|
+
else [bin_path, "--no-names"]
|
|
239
|
+
)
|
|
240
|
+
res = subprocess.run(
|
|
241
|
+
cmd,
|
|
242
|
+
input=content_to_hash,
|
|
243
|
+
capture_output=True,
|
|
244
|
+
check=True,
|
|
245
|
+
)
|
|
246
|
+
out = res.stdout.decode("utf-8", errors="replace").strip()
|
|
247
|
+
if out:
|
|
248
|
+
# Attempt to extract the hash value cleanly
|
|
249
|
+
return out.split()[0]
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.debug(f"BLAKE3 fallback failed with {bin_path}: {e}")
|
|
252
|
+
|
|
253
|
+
# Fallback to pure Python if all external methods fail
|
|
254
|
+
logger.info("Falling back to pure Python implementation for BLAKE3.")
|
|
255
|
+
return pure_python_blake3(content_to_hash)
|
|
256
|
+
|
|
257
|
+
hash_functions = {
|
|
258
|
+
HashAlgorithm.MD5: hashlib.md5,
|
|
259
|
+
HashAlgorithm.SHA1: hashlib.sha1,
|
|
260
|
+
HashAlgorithm.SHA224: hashlib.sha224,
|
|
261
|
+
HashAlgorithm.SHA256: hashlib.sha256,
|
|
262
|
+
HashAlgorithm.SHA384: hashlib.sha384,
|
|
263
|
+
HashAlgorithm.SHA512: hashlib.sha512,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if hash_algorithm not in hash_functions:
|
|
267
|
+
raise ValueError(f"'{hash_algorithm}' is not a valid HashAlgorithm")
|
|
268
|
+
|
|
269
|
+
return hash_functions[hash_algorithm](content_to_hash).hexdigest()
|
|
270
|
+
|
|
271
|
+
def validate(self, expected_hash: str = None):
|
|
272
|
+
"""Validate that the stored hash matches the expected hash"""
|
|
273
|
+
if expected_hash is not None:
|
|
274
|
+
self.hash_value = expected_hash
|
|
275
|
+
|
|
276
|
+
target = self.hash_value
|
|
277
|
+
algo = self.hash_algorithm
|
|
278
|
+
|
|
279
|
+
# Attempt to parse combined hash format (prefix:bare)
|
|
280
|
+
if isinstance(target, str) and ":" in target:
|
|
281
|
+
try:
|
|
282
|
+
prefix, bare = target.split(":", 1)
|
|
283
|
+
# Assuming HashAlgorithm.from_string exists and works if prefix is valid
|
|
284
|
+
if hasattr(HashAlgorithm, "from_string"):
|
|
285
|
+
algo = HashAlgorithm.from_string(prefix)
|
|
286
|
+
target = bare
|
|
287
|
+
else:
|
|
288
|
+
logger.warning(
|
|
289
|
+
"HashAlgorithm.from_string not available. Using default parsing."
|
|
290
|
+
)
|
|
291
|
+
except ValueError:
|
|
292
|
+
pass # Keep original algo/target if parsing fails
|
|
293
|
+
|
|
294
|
+
calculated_hash = self.compute_hash(self.content, algo)
|
|
295
|
+
logger.debug(
|
|
296
|
+
f"Validation Check: expected '{self.hash_value}', calculated '{calculated_hash}'"
|
|
297
|
+
)
|
|
298
|
+
logger.debug(
|
|
299
|
+
f"Hash calculation details: content='{self.content}', hash_function='{algo}'"
|
|
300
|
+
)
|
|
301
|
+
if calculated_hash != target:
|
|
302
|
+
raise ValueError(
|
|
303
|
+
f"Hash mismatch: expected '{self.hash_value}', got '{calculated_hash}'"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def __str__(self):
|
|
307
|
+
return self.hash_value
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Content type detection and validation service.
|
|
3
|
+
|
|
4
|
+
This module provides a unified interface for content type detection and validation
|
|
5
|
+
using a modular architecture with separate concerns for detection strategies,
|
|
6
|
+
validation, and content analysis utilities.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from defusedxml.ElementTree import fromstring as safe_fromstring
|
|
14
|
+
except ImportError:
|
|
15
|
+
from xml.etree.ElementTree import fromstring as safe_fromstring # nosec B314
|
|
16
|
+
|
|
17
|
+
# Import modular components
|
|
18
|
+
from .validators.validation_registry import ValidationError, validation_registry
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import mcard_core_py # type: ignore
|
|
22
|
+
except ImportError:
|
|
23
|
+
LOGGER = logging.getLogger(__name__)
|
|
24
|
+
LOGGER.warning(
|
|
25
|
+
"mcard_core_py native extension not found, functionality may be degraded."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Module logger
|
|
29
|
+
LOGGER = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Re-export ValidationError for backward compatibility
|
|
32
|
+
__all__ = ["ContentTypeInterpreter", "ValidationError", "get_interpreter"]
|
|
33
|
+
|
|
34
|
+
# Module-level singleton instance (lazy)
|
|
35
|
+
_default_interpreter: "ContentTypeInterpreter | None" = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_interpreter() -> "ContentTypeInterpreter":
|
|
39
|
+
"""Return the module-level ContentTypeInterpreter singleton."""
|
|
40
|
+
global _default_interpreter
|
|
41
|
+
if _default_interpreter is None:
|
|
42
|
+
_default_interpreter = ContentTypeInterpreter()
|
|
43
|
+
return _default_interpreter
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ContentTypeInterpreter:
|
|
47
|
+
"""
|
|
48
|
+
Unified service for content type detection and validation.
|
|
49
|
+
|
|
50
|
+
This consolidated class uses modular components internally while maintaining
|
|
51
|
+
the same external API for backward compatibility.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# ─── Load shared MIME data from external JSON ─────────────────────────
|
|
55
|
+
# The canonical data lives in mime_extensions.json at the repo root.
|
|
56
|
+
# Both Python and TypeScript load from it, keeping them in sync.
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _load_shared_mime_data() -> tuple[frozenset, dict]:
|
|
59
|
+
"""Load MIME data from the shared JSON file.
|
|
60
|
+
|
|
61
|
+
Returns a (text_mime_types frozenset, mime_to_extension dict) tuple.
|
|
62
|
+
Falls back to minimal built-in defaults if the file is not found.
|
|
63
|
+
"""
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
|
|
66
|
+
# Walk up from this file to find mime_extensions.json at repo root
|
|
67
|
+
search = Path(__file__).resolve().parent
|
|
68
|
+
for _ in range(5):
|
|
69
|
+
candidate = search / "mime_extensions.json"
|
|
70
|
+
if candidate.exists():
|
|
71
|
+
break
|
|
72
|
+
search = search.parent
|
|
73
|
+
else:
|
|
74
|
+
candidate = None
|
|
75
|
+
|
|
76
|
+
if candidate and candidate.exists():
|
|
77
|
+
try:
|
|
78
|
+
with open(candidate, encoding="utf-8") as f:
|
|
79
|
+
data = json.load(f)
|
|
80
|
+
text_types = frozenset(data.get("textMimeTypes", []))
|
|
81
|
+
ext_map = dict(data.get("mimeToExtension", {}))
|
|
82
|
+
return text_types, ext_map
|
|
83
|
+
except Exception as e:
|
|
84
|
+
LOGGER.warning(
|
|
85
|
+
"Failed to load mime_extensions.json: %s — using defaults", e
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Minimal fallback if JSON file is missing
|
|
89
|
+
return frozenset({"text/plain", "application/json"}), {
|
|
90
|
+
"text/plain": "txt",
|
|
91
|
+
"application/json": "json",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Populated from the shared JSON (or fallback)
|
|
95
|
+
_text_types, _ext_map = _load_shared_mime_data.__func__() # type: ignore[attr-defined]
|
|
96
|
+
TEXT_MIME_TYPES: frozenset = _text_types
|
|
97
|
+
_MIME_TO_EXTENSION: dict = _ext_map
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def register_extension(cls, mime_type: str, extension: str) -> None:
|
|
101
|
+
"""Dynamically register a new MIME-to-extension mapping.
|
|
102
|
+
|
|
103
|
+
Accepts extensions with or without a leading dot (e.g., '.custom' or 'custom').
|
|
104
|
+
The stored value will always be without a leading dot, consistent with the existing mapping.
|
|
105
|
+
"""
|
|
106
|
+
if extension.startswith("."):
|
|
107
|
+
extension = extension[1:]
|
|
108
|
+
cls._MIME_TO_EXTENSION[mime_type] = extension
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def register_extensions(cls, mapping: dict) -> None:
|
|
112
|
+
"""Dynamically register a batch of MIME-to-extension mappings."""
|
|
113
|
+
for mime, ext in mapping.items():
|
|
114
|
+
cls.register_extension(mime, ext)
|
|
115
|
+
|
|
116
|
+
def __init__(self) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Initialize the interpreter.
|
|
119
|
+
"""
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _decode_utf8(data: bytes) -> str:
|
|
124
|
+
"""Decode bytes to UTF-8 string using a consistent policy."""
|
|
125
|
+
return data.decode("utf-8", errors="replace")
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def detect_content_type(
|
|
129
|
+
content: str | bytes, file_extension: str | None = None
|
|
130
|
+
) -> tuple[str, str]:
|
|
131
|
+
"""
|
|
132
|
+
Detect content type and suggested extension using Rust Core.
|
|
133
|
+
"""
|
|
134
|
+
if isinstance(content, str):
|
|
135
|
+
content = content.encode("utf-8")
|
|
136
|
+
return mcard_core_py.detect_content_type(content, file_extension) # type: ignore
|
|
137
|
+
|
|
138
|
+
def detect_content_type_with_strategy(
|
|
139
|
+
self, content: str | bytes, file_extension: str | None = None
|
|
140
|
+
) -> tuple[str, str]:
|
|
141
|
+
"""
|
|
142
|
+
Detect content type using the configured strategy.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
content: The content to analyze
|
|
146
|
+
file_extension: Optional file extension hint
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Tuple of (mime_type, extension)
|
|
150
|
+
"""
|
|
151
|
+
return self.detect_content_type(content, file_extension)
|
|
152
|
+
|
|
153
|
+
def validate_content(self, content: str | bytes) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Validate content based on its detected type.
|
|
156
|
+
|
|
157
|
+
This method now delegates to the validation registry for better modularity.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
content: The content to validate
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
ValidationError: If content is invalid
|
|
164
|
+
"""
|
|
165
|
+
mime_type, _ = self.detect_content_type(content)
|
|
166
|
+
validation_registry.validate(content, mime_type)
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def validate_content_static(content: str | bytes) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Static method for validating content (backward compatibility).
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
content: The content to validate
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ValidationError: If content is invalid
|
|
178
|
+
"""
|
|
179
|
+
# Use default strategy for static method
|
|
180
|
+
mime_type, _ = ContentTypeInterpreter.detect_content_type(content)
|
|
181
|
+
validation_registry.validate(content, mime_type)
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def is_binary_content(content: str | bytes) -> bool:
|
|
185
|
+
"""
|
|
186
|
+
Determine if content should be treated as binary using the Rust Core.
|
|
187
|
+
"""
|
|
188
|
+
if isinstance(content, str):
|
|
189
|
+
content = content.encode("utf-8")
|
|
190
|
+
return mcard_core_py.is_binary_content(content) # type: ignore
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def is_xml_content(content: str | bytes) -> bool:
|
|
194
|
+
"""Check if content is valid XML."""
|
|
195
|
+
# Delegate to XMLDetector's is_valid_xml method
|
|
196
|
+
try:
|
|
197
|
+
if isinstance(content, str):
|
|
198
|
+
content = content.encode("utf-8")
|
|
199
|
+
|
|
200
|
+
# Try to parse the XML without requiring XML declaration
|
|
201
|
+
safe_fromstring(content) # nosec B314
|
|
202
|
+
LOGGER.debug("Valid XML content detected.")
|
|
203
|
+
return True
|
|
204
|
+
except Exception as e:
|
|
205
|
+
LOGGER.debug("Invalid XML content detected: %s", str(e))
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def is_svg_content(content: str | bytes) -> bool:
|
|
210
|
+
"""Check if content is SVG."""
|
|
211
|
+
if isinstance(content, bytes):
|
|
212
|
+
content = content.decode("utf-8", errors="ignore")
|
|
213
|
+
|
|
214
|
+
# First check if it's valid XML
|
|
215
|
+
if not ContentTypeInterpreter.is_xml_content(content):
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
# Parse XML and check for SVG namespace
|
|
220
|
+
tree = safe_fromstring(content) # nosec B314
|
|
221
|
+
return (
|
|
222
|
+
tree.tag == "svg"
|
|
223
|
+
or tree.tag.endswith("}svg")
|
|
224
|
+
or any(
|
|
225
|
+
attr.endswith("xmlns") and "svg" in value
|
|
226
|
+
for attr, value in tree.attrib.items()
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
except Exception:
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def is_mermaid_content(content: str) -> bool:
|
|
234
|
+
"""Check if content is Mermaid diagram."""
|
|
235
|
+
content = content.strip().lower()
|
|
236
|
+
mermaid_keywords = [
|
|
237
|
+
"graph ",
|
|
238
|
+
"sequencediagram",
|
|
239
|
+
"classDiagram",
|
|
240
|
+
"stateDiagram",
|
|
241
|
+
"erDiagram",
|
|
242
|
+
"gantt",
|
|
243
|
+
"pie",
|
|
244
|
+
"flowchart",
|
|
245
|
+
"journey",
|
|
246
|
+
]
|
|
247
|
+
return any(content.startswith(keyword.lower()) for keyword in mermaid_keywords)
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def is_diagram_content(content: str) -> bool:
|
|
251
|
+
"""Check if content is a diagram format."""
|
|
252
|
+
content = content.strip().lower()
|
|
253
|
+
# Check for PlantUML
|
|
254
|
+
if content.startswith("@startuml") and content.endswith("@enduml"):
|
|
255
|
+
return True
|
|
256
|
+
# Check for Graphviz
|
|
257
|
+
if content.startswith(("digraph", "graph", "strict")):
|
|
258
|
+
return True
|
|
259
|
+
# Check for Mermaid
|
|
260
|
+
return ContentTypeInterpreter.is_mermaid_content(content)
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
def get_extension(mime_type: str) -> str:
|
|
264
|
+
"""Get file extension from MIME type using Rust Core."""
|
|
265
|
+
ext = mcard_core_py.get_extension(mime_type)
|
|
266
|
+
if not ext:
|
|
267
|
+
ext = ContentTypeInterpreter.get_default_extension(mime_type)
|
|
268
|
+
return ext if ext is not None else ""
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def get_default_extension(mime_type: str) -> str:
|
|
272
|
+
"""
|
|
273
|
+
Return the default file extension for a given MIME type.
|
|
274
|
+
"""
|
|
275
|
+
return ContentTypeInterpreter._MIME_TO_EXTENSION.get(mime_type, "")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Pagination support for MCard collections."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Page(Generic[T]):
|
|
11
|
+
"""A page of search results."""
|
|
12
|
+
|
|
13
|
+
items: list[T]
|
|
14
|
+
page_number: int
|
|
15
|
+
page_size: int
|
|
16
|
+
total_items: int
|
|
17
|
+
total_pages: int
|
|
18
|
+
has_next: bool
|
|
19
|
+
has_previous: bool
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def create(
|
|
23
|
+
cls, items: list, total_items: int, page_number: int, page_size: int
|
|
24
|
+
) -> "Page":
|
|
25
|
+
"""Factory that computes has_next, has_previous, and total_pages automatically."""
|
|
26
|
+
total_pages = (total_items + page_size - 1) // page_size if page_size > 0 else 0
|
|
27
|
+
return cls(
|
|
28
|
+
items=items,
|
|
29
|
+
total_items=total_items,
|
|
30
|
+
page_number=page_number,
|
|
31
|
+
page_size=page_size,
|
|
32
|
+
total_pages=total_pages,
|
|
33
|
+
has_next=total_items > page_number * page_size,
|
|
34
|
+
has_previous=page_number > 1,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def next_page_number(self) -> int | None:
|
|
39
|
+
"""Get the next page number, or None if this is the last page."""
|
|
40
|
+
return self.page_number + 1 if self.has_next else None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def previous_page_number(self) -> int | None:
|
|
44
|
+
"""Get the previous page number, or None if this is the first page."""
|
|
45
|
+
return self.page_number - 1 if self.has_previous else None
|