openom-core 0.1.0__tar.gz
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.
- openom_core-0.1.0/.gitignore +62 -0
- openom_core-0.1.0/.hypothesis/.gitignore +9 -0
- openom_core-0.1.0/.hypothesis/constants/001fe83bc75f70ff +4 -0
- openom_core-0.1.0/.hypothesis/constants/17983501d9df90f5 +4 -0
- openom_core-0.1.0/.hypothesis/constants/1bad5597e3545771 +4 -0
- openom_core-0.1.0/.hypothesis/constants/1e120eed62cb9e41 +4 -0
- openom_core-0.1.0/.hypothesis/constants/2599456d76b7b639 +4 -0
- openom_core-0.1.0/.hypothesis/constants/2599a2740a02ac70 +4 -0
- openom_core-0.1.0/.hypothesis/constants/2bbc47f0d389ac49 +4 -0
- openom_core-0.1.0/.hypothesis/constants/2dc28f4fae47c5df +4 -0
- openom_core-0.1.0/.hypothesis/constants/391bb7a7db122863 +4 -0
- openom_core-0.1.0/.hypothesis/constants/3b99d738d4809a52 +4 -0
- openom_core-0.1.0/.hypothesis/constants/3bee2e16f7a8fc8e +4 -0
- openom_core-0.1.0/.hypothesis/constants/517da07006529f54 +4 -0
- openom_core-0.1.0/.hypothesis/constants/5273145f2f12a42a +4 -0
- openom_core-0.1.0/.hypothesis/constants/8a59fd8a0a527e9a +4 -0
- openom_core-0.1.0/.hypothesis/constants/b82be5d38e8f5622 +4 -0
- openom_core-0.1.0/.hypothesis/constants/ccfdabc45d5fe4c6 +4 -0
- openom_core-0.1.0/.hypothesis/constants/d7dad3c83a335ac4 +4 -0
- openom_core-0.1.0/.hypothesis/constants/de803f019b0b542d +4 -0
- openom_core-0.1.0/.hypothesis/constants/e1e4fe9940bd1f03 +4 -0
- openom_core-0.1.0/.hypothesis/constants/ec0635288f17334e +4 -0
- openom_core-0.1.0/.hypothesis/constants/fa756589e5af3e25 +4 -0
- openom_core-0.1.0/.hypothesis/tmp/tmpexu8bi5n +0 -0
- openom_core-0.1.0/.hypothesis/tmp/tmpxlvp8s0d +0 -0
- openom_core-0.1.0/.hypothesis/unicode_data/14.0.0/charmap.json.gz +0 -0
- openom_core-0.1.0/.hypothesis/unicode_data/14.0.0/codec-utf-8.json.gz +0 -0
- openom_core-0.1.0/.hypothesis/unicode_data/14.0.0/codec-v2-utf-8.json.gz +0 -0
- openom_core-0.1.0/PKG-INFO +55 -0
- openom_core-0.1.0/README.md +20 -0
- openom_core-0.1.0/pyproject.toml +111 -0
- openom_core-0.1.0/scripts/characterize_corpus.py +71 -0
- openom_core-0.1.0/scripts/gen_fuzz_corpus.py +87 -0
- openom_core-0.1.0/scripts/gen_vectors.py +197 -0
- openom_core-0.1.0/src/openom_core/__init__.py +65 -0
- openom_core-0.1.0/src/openom_core/canonical.py +166 -0
- openom_core-0.1.0/src/openom_core/embed.py +437 -0
- openom_core-0.1.0/src/openom_core/errors.py +57 -0
- openom_core-0.1.0/src/openom_core/images.py +181 -0
- openom_core-0.1.0/src/openom_core/inspect.py +218 -0
- openom_core-0.1.0/src/openom_core/om-0.1.schema.json +214 -0
- openom_core-0.1.0/src/openom_core/py.typed +0 -0
- openom_core-0.1.0/src/openom_core/schema.py +37 -0
- openom_core-0.1.0/src/openom_core/summary.py +133 -0
- openom_core-0.1.0/src/openom_core/text.py +130 -0
- openom_core-0.1.0/src/openom_core/types.py +75 -0
- openom_core-0.1.0/src/openom_core/validate.py +475 -0
- openom_core-0.1.0/src/openom_core/xmp.py +214 -0
- openom_core-0.1.0/tests/_make_rich.py +43 -0
- openom_core-0.1.0/tests/_make_scan.py +75 -0
- openom_core-0.1.0/tests/_render.py +62 -0
- openom_core-0.1.0/tests/conftest.py +79 -0
- openom_core-0.1.0/tests/fixtures/gen_producer_fixtures.py +86 -0
- openom_core-0.1.0/tests/fixtures/gen_signed_fixture.py +83 -0
- openom_core-0.1.0/tests/fixtures/producers/producer-hybrid.pdf +108 -0
- openom_core-0.1.0/tests/fixtures/producers/producer-native.pdf +0 -0
- openom_core-0.1.0/tests/fixtures/producers/producer-scanned.pdf +250 -0
- openom_core-0.1.0/tests/fixtures/signed-approval.pdf +123 -0
- openom_core-0.1.0/tests/test_canonical.py +197 -0
- openom_core-0.1.0/tests/test_classify_eval.py +44 -0
- openom_core-0.1.0/tests/test_codes.py +29 -0
- openom_core-0.1.0/tests/test_consistency.py +97 -0
- openom_core-0.1.0/tests/test_consistency_real_om.py +86 -0
- openom_core-0.1.0/tests/test_crossimpl.py +69 -0
- openom_core-0.1.0/tests/test_decrypt_crossimpl.py +34 -0
- openom_core-0.1.0/tests/test_embed.py +112 -0
- openom_core-0.1.0/tests/test_embed_edges.py +120 -0
- openom_core-0.1.0/tests/test_format_assertion.py +33 -0
- openom_core-0.1.0/tests/test_fuzz.py +27 -0
- openom_core-0.1.0/tests/test_images.py +315 -0
- openom_core-0.1.0/tests/test_inspect.py +88 -0
- openom_core-0.1.0/tests/test_negatives.py +26 -0
- openom_core-0.1.0/tests/test_nondestructive.py +123 -0
- openom_core-0.1.0/tests/test_pathology_vectors.py +46 -0
- openom_core-0.1.0/tests/test_producers.py +91 -0
- openom_core-0.1.0/tests/test_reembed.py +148 -0
- openom_core-0.1.0/tests/test_rejections.py +32 -0
- openom_core-0.1.0/tests/test_render_extra.py +50 -0
- openom_core-0.1.0/tests/test_rfc8785.py +35 -0
- openom_core-0.1.0/tests/test_samples.py +68 -0
- openom_core-0.1.0/tests/test_schema.py +19 -0
- openom_core-0.1.0/tests/test_schema_packaged.py +36 -0
- openom_core-0.1.0/tests/test_signed_embed.py +137 -0
- openom_core-0.1.0/tests/test_smoke.py +15 -0
- openom_core-0.1.0/tests/test_summary.py +46 -0
- openom_core-0.1.0/tests/test_summary_crossimpl.py +50 -0
- openom_core-0.1.0/tests/test_survival.py +97 -0
- openom_core-0.1.0/tests/test_text.py +88 -0
- openom_core-0.1.0/tests/test_types.py +45 -0
- openom_core-0.1.0/tests/test_validate.py +313 -0
- openom_core-0.1.0/tests/test_validate_default_schema.py +43 -0
- openom_core-0.1.0/tests/test_vectors.py +81 -0
- openom_core-0.1.0/tests/test_xmp.py +169 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# --- OpenOM repo hygiene (see CLAUDE.md Rule 2) ---
|
|
2
|
+
|
|
3
|
+
# Marketing mock - NEVER committed
|
|
4
|
+
OpenOM - Make your deal legible to the buy-side's AI.htm
|
|
5
|
+
OpenOM - Make your deal legible to the buy-side's AI_files/
|
|
6
|
+
|
|
7
|
+
# Planning scratch - git-ignored by rule
|
|
8
|
+
docs/superpowers/
|
|
9
|
+
.planning/
|
|
10
|
+
|
|
11
|
+
# Eval / tooling scratch
|
|
12
|
+
.deepeval/
|
|
13
|
+
|
|
14
|
+
# Real OM corpus - private test fixtures, never committed (confidential)
|
|
15
|
+
OMs/
|
|
16
|
+
|
|
17
|
+
# --- Python (/core, /cli, /mcp) ---
|
|
18
|
+
__pycache__/
|
|
19
|
+
*.py[cod]
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.eggs/
|
|
22
|
+
build/
|
|
23
|
+
dist/
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
.env
|
|
27
|
+
.mypy_cache/
|
|
28
|
+
.ruff_cache/
|
|
29
|
+
.pytest_cache/
|
|
30
|
+
.coverage
|
|
31
|
+
htmlcov/
|
|
32
|
+
|
|
33
|
+
# --- Node / TS (/js, /extension) ---
|
|
34
|
+
node_modules/
|
|
35
|
+
*.tsbuildinfo
|
|
36
|
+
.npm/
|
|
37
|
+
extension/src/generated/
|
|
38
|
+
extension/test-results/
|
|
39
|
+
extension/playwright-report/
|
|
40
|
+
|
|
41
|
+
# --- OS / editor ---
|
|
42
|
+
.DS_Store
|
|
43
|
+
Thumbs.db
|
|
44
|
+
.idea/
|
|
45
|
+
.vscode/
|
|
46
|
+
coverage/
|
|
47
|
+
extension/*.zip
|
|
48
|
+
|
|
49
|
+
js/test-results/
|
|
50
|
+
|
|
51
|
+
# marketing mock (Rule 2 - never commit)
|
|
52
|
+
*.htm
|
|
53
|
+
*_files/
|
|
54
|
+
.playwright-mcp/
|
|
55
|
+
om-normative-spec-draft-v0.1.md
|
|
56
|
+
om-standard-handoff-v4-updated.md
|
|
57
|
+
recon.md
|
|
58
|
+
openOM-decision-memo.md
|
|
59
|
+
|
|
60
|
+
# audit/backlog scratch is temp - lives in .planning/ (see Rule 2)
|
|
61
|
+
/BACKLOG.md
|
|
62
|
+
/*-audit*.md
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# This .gitignore file was automatically created by Hypothesis. Hypothesis gitignores
|
|
2
|
+
# .hypothesis by default, because we generally recommend that .hypothesis not be checked
|
|
3
|
+
# into version control.
|
|
4
|
+
#
|
|
5
|
+
# If you *would* like to check .hypothesis into version control, you should delete this
|
|
6
|
+
# file. Hypothesis will not re-create this .gitignore unless .hypothesis is deleted (and
|
|
7
|
+
# if it does, that's a bug - please report it!)
|
|
8
|
+
|
|
9
|
+
*
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\xmp.py
|
|
2
|
+
# hypothesis_version: 6.155.2
|
|
3
|
+
|
|
4
|
+
[b'\xef\xbb\xbf', ' </rdf:RDF>', '&', '&', '>', '<', '/Metadata', '<', '</rdf:RDF>', '>', 'assertedDate', 'omspec', 'openOM', 'payloadFilename', 'payloadHash', 'replace', 'sourceDocHash', 'specName', 'specVersion', 'supersedes', 'utf-8', '{', '}']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\__init__.py
|
|
2
|
+
# hypothesis_version: 6.155.2
|
|
3
|
+
|
|
4
|
+
['0.0.0+unknown', '0.1', 'DealSummary', 'Finding', 'ImageManifest', 'OMPayload', 'PayloadTooLargeError', 'Profile', 'ReadResult', 'RealEstateListing', 'Report', 'SPEC_VERSION', 'Severity', 'TextResult', 'Tolerances', '__version__', 'canonicalize', 'classify', 'embed', 'extract_images', 'extract_text', 'hash_bytes', 'load_schema', 'openom-core', 'payload_hash', 'read', 'reembed_warnings', 'summarize_deal', 'validate']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\images.py
|
|
2
|
+
# hypothesis_version: 6.165.9
|
|
3
|
+
|
|
4
|
+
[100000000, 'DeviceGray', 'DeviceRGB', 'colorspace', 'contentHash', 'deduped', 'error', 'hasSMask', 'height', 'image/png', 'images', 'mime', 'path', 'pdf', 'sha256:', 'unknown', 'width', 'xref']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\images.py
|
|
2
|
+
# hypothesis_version: 6.155.2
|
|
3
|
+
|
|
4
|
+
[1.0, 2.0, 100000000, 'DeviceGray', 'DeviceRGB', 'colorspace', 'contentHash', 'deduped', 'error', 'hasSMask', 'height', 'image/png', 'images', 'mime', 'page', 'path', 'pdf', 'rendered-page', 'sha256:', 'source', 'unknown', 'width', 'xref']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\inspect.py
|
|
2
|
+
# hypothesis_version: 6.155.2
|
|
3
|
+
|
|
4
|
+
[0.0, 0.2, 0.5, 0.6, 0.8, 0.85, 1.0, 3.0, 100, 'Profile', 'bbox', 'chars', 'class', 'classConfidence', 'colorspaces', 'count', 'hasSMask', 'hashValid', 'hybrid', 'images', 'native', 'ocrOverlay', 'pages', 'payload', 'pdf', 'present', 'scanned', 'specVersion', 'text', 'textCoverage', 'type', 'unknown']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\xmp.py
|
|
2
|
+
# hypothesis_version: 6.165.9
|
|
3
|
+
|
|
4
|
+
[b'\xef\xbb\xbf', ' </rdf:RDF>', '&', '&', '>', '<', '/Metadata', '<', '</rdf:RDF>', '>', 'assertedDate', 'omspec', 'openOM', 'payloadFilename', 'payloadHash', 'replace', 'specName', 'specVersion', 'supersedes', 'utf-8', '{', '}']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\summary.py
|
|
2
|
+
# hypothesis_version: 6.155.2
|
|
3
|
+
|
|
4
|
+
[', ', 'USD', 'address', 'addressLocality', 'addressRegion', 'askingPrice', 'assertedBy', 'assertedDate', 'broker', 'brokerage', 'buildingSF', 'capRate', 'commencement', 'currency', 'deal', 'expiration', 'lease', 'leaseTypeAsserted', 'license', 'noi', 'noiAsOfDate', 'noiType', 'postalCode', 'pricePerSF', 'pricePerUnit', 'property', 'propertyType', 'streetAddress', 'tenantEntity', 'termMonths', 'units']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\embed.py
|
|
2
|
+
# hypothesis_version: 6.165.9
|
|
3
|
+
|
|
4
|
+
[5000000, '/AF', '/DP', '/DecodeParms', '/EF', '/F', '/Filter', '/FlateDecode', '/UF', '/application/ld+json', '/assertedDate', '0.1', 'OMW-W051', 'application/ld+json', 'assertedDate', 'om.json', 'payloadHash', 'specVersion', 'supersedes', 'warning']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\validate.py
|
|
2
|
+
# hypothesis_version: 6.165.9
|
|
3
|
+
|
|
4
|
+
[0.005, 0.01, 0.02, 0.2, 30.4375, 31.0, "'", '-', '/', '/assertedDate', '/currency', '/deal', '/deal/askingPrice', '/deal/capRate', '/deal/noi', '/deal/noiAsOfDate', '/deal/pricePerSF', '/lease/expiration', '/lease/rentSchedule', '/lease/termMonths', '/meta/signature', '/meta/supersedes', '/property/buildingSF', 'E', 'I', 'NN', 'NNN', 'OM-CONS-010', 'OM-CONS-011', 'OM-CONS-012', 'OM-CONS-013', 'OM-CONS-014', 'OM-CONS-020', 'OM-CONS-021', 'OM-CONS-022', 'OM-CONS-023', 'OM-CONS-024', 'OM-CONS-025', 'OM-CONS-026', 'OM-CONS-030', 'OM-CONS-031', 'OM-CONS-032', 'OM-CONS-033', 'OM-CONS-034', 'OM-CONS-040', 'OM-CONS-041', 'OM-CONS-050', 'OM-CONS-060', 'OM-DD', 'OM-DD-001', 'OM-DD-002', 'OM-DD-004', 'OM-ERR-013', 'OM-ERR-014', 'OM-ERR-090', 'OMI-I001', 'OMI-I002', 'OMI-I003', 'OMV-E001', 'OMV-E002', 'OMV-E003', 'OMV-E010', 'OMW-W010', 'OMW-W011', 'OMW-W012', 'OMW-W013', 'OMW-W014', 'OMW-W020', 'OMW-W021', 'OMW-W022', 'OMW-W023', 'OMW-W024', 'OMW-W025', 'OMW-W026', 'OMW-W030', 'OMW-W031', 'OMW-W032', 'OMW-W033', 'OMW-W034', 'OMW-W040', 'OMW-W041', 'OMW-W050', 'OMW-W060', 'W', 'absolute-net', 'annualRent', 'askingPrice', 'assertedDate', 'buildingSF', 'cam', 'capRate', 'commencement', 'currency', 'deal', 'error', 'escalationFromPrior', 'expiration', 'gross', 'hvac', 'in-place', 'inf', 'info', 'insurance', 'lease', 'leaseTypeAsserted', 'meta', 'modified-gross', 'monthlyRent', 'noi', 'noiAsOfDate', 'noiType', 'parking', 'periodEnd', 'periodStart', 'pricePerSF', 'pro-forma', 'property', 'remainingTermMonths', 'rentPSF', 'rentSchedule', 'required', 'roof', 'signature', 'source', 'structure', 'supersedes', 'taxes', 'termMonths', 'verified', 'warning']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\inspect.py
|
|
2
|
+
# hypothesis_version: 6.165.9
|
|
3
|
+
|
|
4
|
+
[0.2, 0.5, 0.8, 0.85, 1.0, 3.0, 100, 'Profile', 'bbox', 'class', 'classConfidence', 'colorspaces', 'count', 'hasSMask', 'hashValid', 'hybrid', 'images', 'native', 'pages', 'payload', 'pdf', 'present', 'scanned', 'specVersion', 'text', 'textCoverage', 'unknown']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\xmp.py
|
|
2
|
+
# hypothesis_version: 6.165.9
|
|
3
|
+
|
|
4
|
+
[b'\xef\xbb\xbf', ' </rdf:RDF>', '&', '&', '>', '<', '/Metadata', '<', '</rdf:RDF>', '>', 'assertedDate', 'omspec', 'openOM', 'payloadFilename', 'payloadHash', 'replace', 'specName', 'specVersion', 'supersedes', 'utf-8', '{', '}']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\validate.py
|
|
2
|
+
# hypothesis_version: 6.155.2
|
|
3
|
+
|
|
4
|
+
[0.005, 0.01, 0.02, 0.2, 30.4375, 31.0, "'", '-', '/', '/assertedDate', '/currency', '/deal', '/deal/askingPrice', '/deal/capRate', '/deal/noi', '/deal/noiAsOfDate', '/deal/pricePerSF', '/lease/expiration', '/lease/rentSchedule', '/lease/termMonths', '/meta/signature', '/meta/supersedes', '/property/buildingSF', 'E', 'I', 'NN', 'NNN', 'OM-CONS-010', 'OM-CONS-011', 'OM-CONS-012', 'OM-CONS-013', 'OM-CONS-014', 'OM-CONS-020', 'OM-CONS-021', 'OM-CONS-022', 'OM-CONS-023', 'OM-CONS-024', 'OM-CONS-025', 'OM-CONS-026', 'OM-CONS-030', 'OM-CONS-031', 'OM-CONS-032', 'OM-CONS-033', 'OM-CONS-034', 'OM-CONS-040', 'OM-CONS-041', 'OM-CONS-050', 'OM-CONS-060', 'OM-DD-001', 'OM-DD-002', 'OM-DD-003', 'OM-DD-004', 'OM-ERR-013', 'OM-ERR-014', 'OM-ERR-090', 'OM-TRUST-009', 'OM-TRUST-010', 'OMI-I001', 'OMI-I002', 'OMI-I003', 'OMV-E001', 'OMV-E002', 'OMV-E003', 'OMV-E010', 'OMW-W010', 'OMW-W011', 'OMW-W012', 'OMW-W013', 'OMW-W014', 'OMW-W020', 'OMW-W021', 'OMW-W022', 'OMW-W023', 'OMW-W024', 'OMW-W025', 'OMW-W026', 'OMW-W030', 'OMW-W031', 'OMW-W032', 'OMW-W033', 'OMW-W034', 'OMW-W040', 'OMW-W041', 'OMW-W050', 'OMW-W051', 'OMW-W052', 'OMW-W060', 'OMW-W061', 'US', 'W', 'absolute-net', 'address', 'addressCountry', 'annualRent', 'askingPrice', 'assertedDate', 'buildingSF', 'cam', 'capRate', 'commencement', 'currency', 'deal', 'error', 'escalationFromPrior', 'expiration', 'gross', 'hvac', 'in-place', 'inf', 'info', 'insurance', 'lease', 'leaseTypeAsserted', 'meta', 'modified-gross', 'monthlyRent', 'noi', 'noiAsOfDate', 'noiType', 'parking', 'periodEnd', 'periodStart', 'pricePerSF', 'pro-forma', 'property', 'remainingTermMonths', 'rentPSF', 'rentSchedule', 'required', 'roof', 'source', 'structure', 'supersedes', 'taxes', 'termMonths', 'verified', 'warning']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: E:\OpenOM\core\src\openom_core\embed.py
|
|
2
|
+
# hypothesis_version: 6.155.2
|
|
3
|
+
|
|
4
|
+
[5000000, '/AF', '/DP', '/DecodeParms', '/EF', '/F', '/Filter', '/FlateDecode', '/UF', '/application/ld+json', '/assertedDate', '0.1', 'OMW-W051', 'application/ld+json', 'assertedDate', 'om.json', 'payloadHash', 'sourceDocHash', 'specVersion', 'supersedes', 'warning']
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: openom-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: openOM deterministic core - embed/read/inspect/validate CRE offering-memorandum payloads. Zero inference, ever.
|
|
5
|
+
Project-URL: Homepage, https://openom.app
|
|
6
|
+
Project-URL: Documentation, https://openom.app/docs/
|
|
7
|
+
Project-URL: Repository, https://github.com/Vervelio-Labs/OpenOM
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: commercial-real-estate,cre,data-standard,factur-x,json-ld,json-schema,offering-memorandum,open-standard,openom,pdf,pdf-a-3,proptech,real-estate
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
15
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: jsonschema>=4.20
|
|
19
|
+
Requires-Dist: pikepdf>=9
|
|
20
|
+
Requires-Dist: rfc8785>=0.1.4
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
24
|
+
Requires-Dist: numpy>=1.26; extra == 'dev'
|
|
25
|
+
Requires-Dist: pillow>=10; extra == 'dev'
|
|
26
|
+
Requires-Dist: pymupdf>=1.24; extra == 'dev'
|
|
27
|
+
Requires-Dist: pypdfium2>=4; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
31
|
+
Requires-Dist: types-jsonschema>=4.20; extra == 'dev'
|
|
32
|
+
Provides-Extra: render
|
|
33
|
+
Requires-Dist: pymupdf>=1.24; extra == 'render'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# openom-core
|
|
37
|
+
|
|
38
|
+
The deterministic Python core of the [openOM](../README.md) standard: the PDF/data verbs
|
|
39
|
+
(`embed`, `read`, `inspect`, `extract`, `validate`) and RFC 8785 canonicalization + SHA-256
|
|
40
|
+
integrity. **Zero inference, ever** (CI's `boundary` job enforces it). Byte-parity with `js/`.
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
# From a clone (openom-core is not yet on PyPI):
|
|
44
|
+
pip install -e core # add the [dev] extra only to contribute
|
|
45
|
+
# Once published: pip install openom-core
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from openom_core.embed import embed, read
|
|
50
|
+
out = embed(pdf_bytes, payload, asserted_date="2026-08-16") # non-destructive
|
|
51
|
+
r = read(out) # r.present / r.hash_valid / r.payload
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Entrypoints: `openom_core.{canonical,embed,validate,inspect,images,text,schema,xmp}`.
|
|
55
|
+
Tests: `pytest core -q --cov=openom_core --cov-fail-under=90`. Schema + vectors live in [`/spec`](../spec).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# openom-core
|
|
2
|
+
|
|
3
|
+
The deterministic Python core of the [openOM](../README.md) standard: the PDF/data verbs
|
|
4
|
+
(`embed`, `read`, `inspect`, `extract`, `validate`) and RFC 8785 canonicalization + SHA-256
|
|
5
|
+
integrity. **Zero inference, ever** (CI's `boundary` job enforces it). Byte-parity with `js/`.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
# From a clone (openom-core is not yet on PyPI):
|
|
9
|
+
pip install -e core # add the [dev] extra only to contribute
|
|
10
|
+
# Once published: pip install openom-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from openom_core.embed import embed, read
|
|
15
|
+
out = embed(pdf_bytes, payload, asserted_date="2026-08-16") # non-destructive
|
|
16
|
+
r = read(out) # r.present / r.hash_valid / r.payload
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Entrypoints: `openom_core.{canonical,embed,validate,inspect,images,text,schema,xmp}`.
|
|
20
|
+
Tests: `pytest core -q --cov=openom_core --cov-fail-under=90`. Schema + vectors live in [`/spec`](../spec).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "openom-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "openOM deterministic core - embed/read/inspect/validate CRE offering-memorandum payloads. Zero inference, ever."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = [
|
|
13
|
+
"openom", "offering-memorandum", "commercial-real-estate", "cre", "real-estate", "proptech",
|
|
14
|
+
"pdf", "pdf-a-3", "factur-x", "json-ld", "json-schema", "open-standard", "data-standard",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Topic :: Office/Business :: Financial",
|
|
22
|
+
"Topic :: Text Processing :: Markup",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
# The embed/read/validate/canonical core is pikepdf-only, so `openom-core` stays MIT-clean. PyMuPDF
|
|
26
|
+
# (AGPL-3.0-or-commercial) powers only extract/inspect/render + the signed-OM incremental save, so it
|
|
27
|
+
# is an OPTIONAL `[render]` extra - a portal / commercial integrator bundling the core does not inherit
|
|
28
|
+
# AGPL unless they opt into rendering.
|
|
29
|
+
dependencies = [
|
|
30
|
+
"pikepdf>=9",
|
|
31
|
+
"rfc8785>=0.1.4",
|
|
32
|
+
"jsonschema>=4.20",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
render = [
|
|
37
|
+
"PyMuPDF>=1.24", # AGPL-3.0-or-commercial - powers extract/inspect/render + signed-incremental embed
|
|
38
|
+
]
|
|
39
|
+
dev = [
|
|
40
|
+
"openom-core[render]", # tests exercise extract/inspect/render, so dev pulls the render extra
|
|
41
|
+
"pytest>=8",
|
|
42
|
+
"pytest-cov>=5",
|
|
43
|
+
"mypy>=1.10",
|
|
44
|
+
"ruff>=0.5",
|
|
45
|
+
"numpy>=1.26",
|
|
46
|
+
"pillow>=10",
|
|
47
|
+
"hypothesis>=6.100",
|
|
48
|
+
"types-jsonschema>=4.20",
|
|
49
|
+
"pypdfium2>=4", # independent 2nd PDF renderer - the differential image cross-check (#167)
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://openom.app"
|
|
54
|
+
Documentation = "https://openom.app/docs/"
|
|
55
|
+
Repository = "https://github.com/Vervelio-Labs/OpenOM"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/openom_core"]
|
|
59
|
+
|
|
60
|
+
# Ship the JSON Schema INSIDE the package (#149) so a pip-installed openom-core/-mcp can load it via
|
|
61
|
+
# importlib.resources - no reliance on an adjacent /spec dir. It is committed as ordinary package data
|
|
62
|
+
# at src/openom_core/om-0.1.schema.json (NOT a force-include from ../spec, which cannot survive into a
|
|
63
|
+
# self-contained sdist -> `python -m build` would fail building the wheel from the sdist). /spec stays
|
|
64
|
+
# the single source of truth; the in-package copy is drift-locked to it by tests/test_schema_packaged.py.
|
|
65
|
+
|
|
66
|
+
[tool.ruff]
|
|
67
|
+
line-length = 100
|
|
68
|
+
src = ["src", "tests"]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint]
|
|
71
|
+
select = ["E", "F", "I", "B", "UP", "W"]
|
|
72
|
+
|
|
73
|
+
[tool.mypy]
|
|
74
|
+
python_version = "3.11"
|
|
75
|
+
strict = true
|
|
76
|
+
files = ["src"]
|
|
77
|
+
|
|
78
|
+
# PyMuPDF ships incomplete type info; treat it as untyped so strict no-untyped-call
|
|
79
|
+
# doesn't fire on its API (we still type our own wrappers around it).
|
|
80
|
+
[[tool.mypy.overrides]]
|
|
81
|
+
module = ["pymupdf", "pymupdf.*"]
|
|
82
|
+
follow_imports = "skip"
|
|
83
|
+
disallow_untyped_calls = false
|
|
84
|
+
|
|
85
|
+
[tool.pytest.ini_options]
|
|
86
|
+
testpaths = ["tests"]
|
|
87
|
+
addopts = "-ra"
|
|
88
|
+
markers = [
|
|
89
|
+
"cross_impl: cross-implementation round-trip ([OM-VEC-002]); needs Node + OPENOM_XIMPL_DIR",
|
|
90
|
+
]
|
|
91
|
+
# Coverage is enforced in CI (pytest --cov=openom_core --cov-fail-under=90), not in the
|
|
92
|
+
# default addopts, so single-file dev runs stay fast and don't trip the floor.
|
|
93
|
+
|
|
94
|
+
[tool.coverage.run]
|
|
95
|
+
source = ["openom_core"]
|
|
96
|
+
branch = true
|
|
97
|
+
|
|
98
|
+
[tool.coverage.report]
|
|
99
|
+
show_missing = true
|
|
100
|
+
exclude_lines = ["pragma: no cover", "raise NotImplementedError", "if TYPE_CHECKING:"]
|
|
101
|
+
|
|
102
|
+
# Mutation testing (#133) - assertion-STRENGTH, beyond line/branch coverage. Targets the correctness-
|
|
103
|
+
# critical modules where a flipped comparison or wrong rounding would produce a divergent-but-passing
|
|
104
|
+
# standard. Runs on a schedule (not per-push); surviving mutants are tracked as coverage debt.
|
|
105
|
+
[tool.mutmut]
|
|
106
|
+
paths_to_mutate = [
|
|
107
|
+
"src/openom_core/canonical.py",
|
|
108
|
+
"src/openom_core/validate.py",
|
|
109
|
+
"src/openom_core/embed.py",
|
|
110
|
+
]
|
|
111
|
+
tests_dir = ["tests/"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Local corpus characterization / classifier-tuning tool (Task 8).
|
|
4
|
+
|
|
5
|
+
Runs ``inspect`` over every PDF under ``OMs/`` (the confidential, git-ignored corpus) and
|
|
6
|
+
prints class, confidence, textCoverage, and page count. Given a labels file
|
|
7
|
+
(JSON ``{relpath: "native"|"hybrid"|"scanned"}``) it also prints a confusion matrix so
|
|
8
|
+
thresholds can be tuned against real ground truth.
|
|
9
|
+
|
|
10
|
+
python core/scripts/characterize_corpus.py [--labels labels.json]
|
|
11
|
+
|
|
12
|
+
Not a CI test: the corpus is not committed, and ground-truth labels require human judgment (Q3).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
from collections import Counter
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from openom_core.inspect import inspect
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
25
|
+
OMS = ROOT / "OMs"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> None:
|
|
29
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
30
|
+
parser.add_argument("--labels", type=Path, default=None, help="JSON {relpath: class}")
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
if not OMS.exists():
|
|
34
|
+
print(f"corpus not present: {OMS}")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
labels: dict[str, str] = (
|
|
38
|
+
json.loads(args.labels.read_text(encoding="utf-8")) if args.labels else {}
|
|
39
|
+
)
|
|
40
|
+
dist: Counter[str] = Counter()
|
|
41
|
+
confusion: Counter[tuple[str, str]] = Counter()
|
|
42
|
+
pdfs = sorted(OMS.rglob("*.pdf"))
|
|
43
|
+
|
|
44
|
+
for pdf in pdfs:
|
|
45
|
+
rel = pdf.relative_to(OMS).as_posix()
|
|
46
|
+
try:
|
|
47
|
+
prof = inspect(pdf.read_bytes())
|
|
48
|
+
except Exception as exc: # noqa: BLE001 - tool: report and keep going
|
|
49
|
+
print(f"ERROR {rel}: {exc}")
|
|
50
|
+
continue
|
|
51
|
+
cls = prof["class"]
|
|
52
|
+
dist[cls] += 1
|
|
53
|
+
print(
|
|
54
|
+
f"{cls:8} conf={prof['classConfidence']:.2f} tc={prof['textCoverage']:.2f} "
|
|
55
|
+
f"pages={prof['pages']:>3} {rel}"
|
|
56
|
+
)
|
|
57
|
+
if rel in labels:
|
|
58
|
+
confusion[(labels[rel], cls)] += 1
|
|
59
|
+
|
|
60
|
+
print(f"\nDISTRIBUTION over {len(pdfs)} files: {dict(dist)}")
|
|
61
|
+
if confusion:
|
|
62
|
+
correct = sum(n for (t, p), n in confusion.items() if t == p)
|
|
63
|
+
total = sum(confusion.values())
|
|
64
|
+
print(f"ACCURACY: {correct}/{total}")
|
|
65
|
+
print("CONFUSION (true -> predicted):")
|
|
66
|
+
for (t, p), n in sorted(confusion.items()):
|
|
67
|
+
print(f" {t:8} -> {p:8}: {n}{'' if t == p else ' <-- MISS'}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Generate the cross-language JCS differential-fuzz corpus (#129).
|
|
4
|
+
|
|
5
|
+
The RFC 8785 canonicalization is the anti-fork keystone: if the Python and TS reference impls ever
|
|
6
|
+
serialize the same value to different bytes, a PDF verifies in one client and fails in another.
|
|
7
|
+
This produces a DETERMINISTIC, edge-weighted corpus of accepted payloads + their canonical hashes.
|
|
8
|
+
Both cores canonicalize the SAME corpus and MUST match `expected` byte-for-byte (test_fuzz.py /
|
|
9
|
+
fuzz.test.ts). The generator is seeded (no Date/urandom) so re-running is a no-op - a diff means a
|
|
10
|
+
hash moved. Run after any canonicalization change: python core/scripts/gen_fuzz_corpus.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import random
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from openom_core.canonical import CanonicalizationError, canonicalize, hash_bytes
|
|
20
|
+
|
|
21
|
+
OUT = Path(__file__).resolve().parents[2] / "spec" / "vectors" / "fuzz"
|
|
22
|
+
SEED = 20260818 # fixed seed -> reproducible corpus (no Date/urandom)
|
|
23
|
+
COUNT = 600
|
|
24
|
+
|
|
25
|
+
# Values that historically fork JCS number/string serialization - the fuzz weights toward these.
|
|
26
|
+
_NUM_EDGES = [
|
|
27
|
+
0, -0.0, 1, -1, 2**53 - 1, -(2**53 - 1), 0.1, -0.1, 0.5, 1.5,
|
|
28
|
+
1e-7, 1.0000001, 9.999999e-7, 0.0000001, 123456.789, 1e-6, 1e-8,
|
|
29
|
+
3.141592653589793, 2.718281828459045, 100.0, 0.0001, 1234567890, 42,
|
|
30
|
+
]
|
|
31
|
+
# Non-ASCII given as escapes so the source file stays pure ASCII (avoids encoding surprises).
|
|
32
|
+
_STR_EDGES = [
|
|
33
|
+
"", "a", "A", "z", "é", "café", # e-acute (NFC precomposed)
|
|
34
|
+
"\U0001f600", "\U0001f4a9 tail", # astral / surrogate-pair strings
|
|
35
|
+
"line1\nline2", "tab\there", 'quote"q', "back\\slash", "slash/fwd",
|
|
36
|
+
"中文", "key sort z", "key sort a",
|
|
37
|
+
]
|
|
38
|
+
_KEY_EDGES = ["a", "b", "A", "z", "é", "\U0001f600key", "10", "2", "中", "Z", "aa"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _rand_value(rng: random.Random, depth: int) -> object:
|
|
42
|
+
if depth >= 4:
|
|
43
|
+
kind = rng.choice(["num", "str", "bool", "null"])
|
|
44
|
+
else:
|
|
45
|
+
kind = rng.choices(
|
|
46
|
+
["num", "str", "bool", "null", "arr", "obj"], weights=[30, 30, 8, 8, 12, 12]
|
|
47
|
+
)[0]
|
|
48
|
+
if kind == "num":
|
|
49
|
+
v = rng.choice(_NUM_EDGES)
|
|
50
|
+
return v if rng.random() < 0.7 else rng.uniform(-1e6, 1e6)
|
|
51
|
+
if kind == "str":
|
|
52
|
+
if rng.random() < 0.8:
|
|
53
|
+
return rng.choice(_STR_EDGES)
|
|
54
|
+
return "".join(rng.choice("abcXYZ é中") for _ in range(rng.randint(0, 8)))
|
|
55
|
+
if kind == "bool":
|
|
56
|
+
return rng.random() < 0.5
|
|
57
|
+
if kind == "null":
|
|
58
|
+
return None
|
|
59
|
+
if kind == "arr":
|
|
60
|
+
return [_rand_value(rng, depth + 1) for _ in range(rng.randint(0, 4))]
|
|
61
|
+
keys = rng.sample(_KEY_EDGES, k=rng.randint(1, min(5, len(_KEY_EDGES))))
|
|
62
|
+
return {k: _rand_value(rng, depth + 1) for k in keys}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> None:
|
|
66
|
+
OUT.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
rng = random.Random(SEED)
|
|
68
|
+
corpus: list[dict] = []
|
|
69
|
+
while len(corpus) < COUNT:
|
|
70
|
+
keys = rng.sample(_KEY_EDGES, k=rng.randint(1, 5))
|
|
71
|
+
obj = {k: _rand_value(rng, 1) for k in keys}
|
|
72
|
+
try:
|
|
73
|
+
canonicalize(obj) # keep only inputs BOTH impls accept (skip range/NFC-dup rejections)
|
|
74
|
+
except CanonicalizationError:
|
|
75
|
+
continue
|
|
76
|
+
corpus.append(obj)
|
|
77
|
+
|
|
78
|
+
dumped = (json.dumps(o, ensure_ascii=False, sort_keys=False) for o in corpus)
|
|
79
|
+
corpus_txt = "\n".join(dumped) + "\n"
|
|
80
|
+
expected_txt = "\n".join(hash_bytes(canonicalize(o)) for o in corpus) + "\n"
|
|
81
|
+
(OUT / "corpus.jsonl").write_text(corpus_txt, encoding="utf-8", newline="\n")
|
|
82
|
+
(OUT / "expected.jsonl").write_text(expected_txt, encoding="utf-8", newline="\n")
|
|
83
|
+
print(f"wrote {len(corpus)} fuzz vectors + expected hashes -> {OUT}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|