openlakeforge-domain-model 0.2.0a1__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.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: openlakeforge-domain-model
3
+ Version: 0.2.0a1
4
+ Summary: Canonical provider-neutral domain model for OpenLakeForge
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: PyYAML<7,>=6.0
@@ -0,0 +1,67 @@
1
+ """Canonical provider-neutral lakehouse descriptors and inventory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .descriptors import (
6
+ LAKEHOUSE_API_VERSION,
7
+ LAKEHOUSE_KIND,
8
+ SOURCE_API_VERSION,
9
+ SOURCE_KIND,
10
+ LakehouseDescriptorError,
11
+ load_lakehouse_descriptor,
12
+ load_source_descriptor,
13
+ validate_lakehouse_descriptor,
14
+ validate_source_descriptor,
15
+ )
16
+ from .inventory import (
17
+ ArtifactPrefixes,
18
+ CatalogNamespace,
19
+ Dashboard,
20
+ Domain,
21
+ LakehouseInventory,
22
+ PhysicalInventory,
23
+ PhysicalDomainNames,
24
+ PhysicalProductNames,
25
+ PhysicalSourceNames,
26
+ Product,
27
+ GoldTable,
28
+ SilverTable,
29
+ SourceResource,
30
+ Source,
31
+ Table,
32
+ inventory_for,
33
+ load_lakehouse_inventory,
34
+ load_lakehouse_inventory_from_descriptors,
35
+ load_transitional_lakehouse_inventory,
36
+ )
37
+
38
+ __all__ = [
39
+ "LAKEHOUSE_API_VERSION",
40
+ "LAKEHOUSE_KIND",
41
+ "SOURCE_API_VERSION",
42
+ "SOURCE_KIND",
43
+ "ArtifactPrefixes",
44
+ "CatalogNamespace",
45
+ "Dashboard",
46
+ "Domain",
47
+ "LakehouseDescriptorError",
48
+ "LakehouseInventory",
49
+ "PhysicalInventory",
50
+ "PhysicalDomainNames",
51
+ "PhysicalProductNames",
52
+ "PhysicalSourceNames",
53
+ "Product",
54
+ "GoldTable",
55
+ "SilverTable",
56
+ "SourceResource",
57
+ "Source",
58
+ "Table",
59
+ "inventory_for",
60
+ "load_lakehouse_descriptor",
61
+ "load_lakehouse_inventory",
62
+ "load_lakehouse_inventory_from_descriptors",
63
+ "load_transitional_lakehouse_inventory",
64
+ "load_source_descriptor",
65
+ "validate_lakehouse_descriptor",
66
+ "validate_source_descriptor",
67
+ ]
@@ -0,0 +1,336 @@
1
+ """Versioned descriptor validation for the Lakehouse and Source kinds.
2
+
3
+ The canonical v1alpha3 model is the ``Lakehouse`` descriptor at
4
+ ``lakehouse_code/lakehouse.yaml`` together with one ``Source`` descriptor per
5
+ bronze source at ``lakehouse_code/bronze/<source>/source.yaml``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from collections.abc import Mapping
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import yaml
16
+
17
+ LAKEHOUSE_API_VERSION = "openlakeforge.io/v1alpha3"
18
+ SOURCE_API_VERSION = "openlakeforge.io/v1alpha3"
19
+ LAKEHOUSE_KIND = "Lakehouse"
20
+ SOURCE_KIND = "Source"
21
+ _PRODUCT_IDENTIFIER_PATTERN = r"[a-z][a-z0-9_]*"
22
+ _IDENTIFIER_PATTERN = re.compile(rf"^{_PRODUCT_IDENTIFIER_PATTERN}$")
23
+
24
+
25
+ class LakehouseDescriptorError(ValueError):
26
+ """Raised when a v1alpha3 lakehouse or source descriptor is invalid."""
27
+
28
+
29
+ def _identifier(value: object, *, field: str, source: str) -> str:
30
+ if not isinstance(value, str) or not _IDENTIFIER_PATTERN.fullmatch(value):
31
+ raise LakehouseDescriptorError(f"{source}: {field} must match '^[a-z][a-z0-9_]*$'")
32
+ return value
33
+
34
+
35
+ def _non_empty_string(value: object, *, field: str, source: str) -> str:
36
+ if not isinstance(value, str) or not value:
37
+ raise LakehouseDescriptorError(f"{source}: {field} must be a non-empty string")
38
+ return value
39
+
40
+
41
+ def _optional_string(value: object, *, field: str, source: str) -> None:
42
+ if value is not None and not isinstance(value, str):
43
+ raise LakehouseDescriptorError(f"{source}: {field} must be a string")
44
+
45
+
46
+ def validate_source_descriptor(document: Mapping[str, Any], *, source: str = "source.yaml") -> None:
47
+ """Validate a v1alpha3 Source descriptor envelope and resource list."""
48
+ if document.get("apiVersion") != SOURCE_API_VERSION:
49
+ raise LakehouseDescriptorError(
50
+ f"{source}: unsupported apiVersion {document.get('apiVersion')!r}; expected {SOURCE_API_VERSION!r}"
51
+ )
52
+ if document.get("kind") != SOURCE_KIND:
53
+ raise LakehouseDescriptorError(f"{source}: kind must be {SOURCE_KIND!r}")
54
+ for field in ("name", "displayName", "description", "status", "resources"):
55
+ if field not in document:
56
+ raise LakehouseDescriptorError(f"{source}: missing required field {field!r}")
57
+ _identifier(document["name"], field="name", source=source)
58
+ _non_empty_string(document["displayName"], field="displayName", source=source)
59
+ _non_empty_string(document["status"], field="status", source=source)
60
+ _optional_string(document["description"], field="description", source=source)
61
+ resources = document["resources"]
62
+ if not isinstance(resources, list) or not resources:
63
+ raise LakehouseDescriptorError(f"{source}: resources must be a non-empty array")
64
+ seen: set[str] = set()
65
+ for index, resource in enumerate(resources):
66
+ if not isinstance(resource, Mapping):
67
+ raise LakehouseDescriptorError(f"{source}: resources[{index}] must be an object")
68
+ if "name" not in resource:
69
+ raise LakehouseDescriptorError(f"{source}: resources[{index}] is missing required field 'name'")
70
+ name = _identifier(resource["name"], field=f"resources[{index}].name", source=source)
71
+ if name in seen:
72
+ raise LakehouseDescriptorError(f"{source}: duplicate resource name {name!r}")
73
+ seen.add(name)
74
+ if "description" in resource and not isinstance(resource["description"], str):
75
+ raise LakehouseDescriptorError(f"{source}: resources[{index}].description must be a string")
76
+ unexpected = set(resource) - {"name", "description"}
77
+ if unexpected:
78
+ raise LakehouseDescriptorError(
79
+ f"{source}: resources[{index}] must not contain {sorted(unexpected)!r} (provider-neutral fields only)"
80
+ )
81
+ unexpected = set(document) - {"apiVersion", "kind", "name", "displayName", "description", "status", "resources"}
82
+ if unexpected:
83
+ raise LakehouseDescriptorError(
84
+ f"{source}: must not contain {sorted(unexpected)!r} (provider-neutral fields only)"
85
+ )
86
+
87
+
88
+ def _validate_table_group(product: Mapping[str, Any], *, group: str, source: str) -> None:
89
+ spec = product.get(group)
90
+ if not isinstance(spec, Mapping):
91
+ raise LakehouseDescriptorError(f"{source}: product {product.get('id')!r}: {group} must be an object")
92
+ tables = spec.get("tables")
93
+ if not isinstance(tables, list) or not tables:
94
+ raise LakehouseDescriptorError(f"{source}: product {product.get('id')!r}: {group}.tables must be a non-empty array")
95
+ seen: set[str] = set()
96
+ for index, table in enumerate(tables):
97
+ if not isinstance(table, Mapping) or not _IDENTIFIER_PATTERN.fullmatch(table.get("name") or ""):
98
+ raise LakehouseDescriptorError(
99
+ f"{source}: product {product.get('id')!r}: {group}.tables[{index}].name must match '^[a-z][a-z0-9_]*$'"
100
+ )
101
+ if table["name"] in seen:
102
+ raise LakehouseDescriptorError(
103
+ f"{source}: product {product.get('id')!r}: {group}.tables must not contain duplicate names"
104
+ )
105
+ seen.add(table["name"])
106
+ if "description" in table and not isinstance(table["description"], str):
107
+ raise LakehouseDescriptorError(f"{source}: product {product.get('id')!r}: {group}.tables[{index}].description must be a string")
108
+ unexpected = set(table) - {"name", "description"}
109
+ if unexpected:
110
+ raise LakehouseDescriptorError(
111
+ f"{source}: product {product.get('id')!r}: {group}.tables[{index}] must not contain "
112
+ f"{sorted(unexpected)!r} (provider-neutral fields only)"
113
+ )
114
+ if "schema" in spec:
115
+ raise LakehouseDescriptorError(
116
+ f"{source}: product {product.get('id')!r}: {group}.schema must be derived from provider contracts"
117
+ )
118
+ unexpected = set(spec) - {"tables"}
119
+ if unexpected:
120
+ raise LakehouseDescriptorError(
121
+ f"{source}: product {product.get('id')!r}: {group} must not contain {sorted(unexpected)!r} "
122
+ "(provider-neutral fields only)"
123
+ )
124
+
125
+
126
+ def _validate_product(product: Mapping[str, Any], *, domain_name: str, source: str) -> None:
127
+ label = product.get("id") or f"product {product!r}"
128
+ for field in ("id", "displayName", "description", "status"):
129
+ if field not in product:
130
+ raise LakehouseDescriptorError(f"{source}: product {label!r}: missing required field {field!r}")
131
+ _identifier(product["id"], field="id", source=f"{source}: product {label!r}")
132
+ _non_empty_string(product["displayName"], field="displayName", source=f"{source}: product {label!r}")
133
+ _non_empty_string(product["status"], field="status", source=f"{source}: product {label!r}")
134
+ _optional_string(product["description"], field="description", source=f"{source}: product {label!r}")
135
+ if "domain" in product and product["domain"] != domain_name:
136
+ raise LakehouseDescriptorError(
137
+ f"{source}: product {label!r}: domain {product['domain']!r} must match enclosing domain {domain_name!r}"
138
+ )
139
+ silver_inputs = product.get("silver_inputs")
140
+ if not isinstance(silver_inputs, list) or not silver_inputs:
141
+ raise LakehouseDescriptorError(f"{source}: product {label!r}: silver_inputs must be a non-empty array")
142
+ if any(not isinstance(value, str) or not _IDENTIFIER_PATTERN.fullmatch(value) for value in silver_inputs):
143
+ raise LakehouseDescriptorError(
144
+ f"{source}: product {label!r}: silver_inputs must contain identifiers matching '^[a-z][a-z0-9_]*$'"
145
+ )
146
+ if len(silver_inputs) != len(set(silver_inputs)):
147
+ raise LakehouseDescriptorError(f"{source}: product {label!r}: silver_inputs must not contain duplicates")
148
+ _validate_table_group(product, group="gold_tables", source=source)
149
+ unexpected = set(product) - {
150
+ "id", "displayName", "description", "status", "domain", "silver_inputs", "gold_tables",
151
+ }
152
+ if unexpected:
153
+ raise LakehouseDescriptorError(
154
+ f"{source}: product {label!r} must not contain {sorted(unexpected)!r} (provider-neutral fields only)"
155
+ )
156
+
157
+
158
+ def _validate_domain(domain: Mapping[str, Any], *, source: str) -> None:
159
+ required = ("name", "displayName", "description", "status", "silver_tables", "products")
160
+ for field in required:
161
+ if field not in domain:
162
+ raise LakehouseDescriptorError(f"{source}: domain: missing required field {field!r}")
163
+ _identifier(domain["name"], field="domain.name", source=source)
164
+ _non_empty_string(domain["displayName"], field="domain.displayName", source=source)
165
+ _non_empty_string(domain["status"], field="domain.status", source=source)
166
+ _optional_string(domain["description"], field="domain.description", source=source)
167
+ _validate_silver_table_group(domain, source=source)
168
+ products = domain["products"]
169
+ if not isinstance(products, list):
170
+ raise LakehouseDescriptorError(f"{source}: domain {domain['name']!r}: products must be an array")
171
+ seen_products: set[str] = set()
172
+ silver_names = {table["name"] for table in domain["silver_tables"]["tables"]}
173
+ for product in products:
174
+ if not isinstance(product, Mapping):
175
+ raise LakehouseDescriptorError(f"{source}: domain {domain['name']!r}: products must contain objects")
176
+ _validate_product(product, domain_name=domain["name"], source=source)
177
+ if product["id"] in seen_products:
178
+ raise LakehouseDescriptorError(
179
+ f"{source}: domain {domain['name']!r}: duplicate product id {product['id']!r}"
180
+ )
181
+ seen_products.add(product["id"])
182
+ unknown_inputs = sorted(set(product["silver_inputs"]) - silver_names)
183
+ if unknown_inputs:
184
+ raise LakehouseDescriptorError(
185
+ f"{source}: product {product['id']!r}: silver_inputs reference unknown domain Silver tables "
186
+ f"{unknown_inputs!r}"
187
+ )
188
+ unexpected = set(domain) - {"name", "displayName", "description", "status", "silver_tables", "products"}
189
+ if unexpected:
190
+ raise LakehouseDescriptorError(
191
+ f"{source}: domain {domain['name']!r} must not contain {sorted(unexpected)!r} (provider-neutral fields only)"
192
+ )
193
+
194
+
195
+ def _validate_silver_table_group(domain: Mapping[str, Any], *, source: str) -> None:
196
+ spec = domain.get("silver_tables")
197
+ if not isinstance(spec, Mapping) or not isinstance(spec.get("tables"), list) or not spec["tables"]:
198
+ raise LakehouseDescriptorError(f"{source}: domain {domain.get('name')!r}: silver_tables.tables must be a non-empty array")
199
+ if set(spec) != {"tables"}:
200
+ unexpected = sorted(set(spec) - {"tables"})
201
+ raise LakehouseDescriptorError(
202
+ f"{source}: domain {domain.get('name')!r}: silver_tables must not contain {unexpected!r} "
203
+ "(provider-neutral fields only)"
204
+ )
205
+ seen: set[str] = set()
206
+ for index, table in enumerate(spec["tables"]):
207
+ if not isinstance(table, Mapping) or not _IDENTIFIER_PATTERN.fullmatch(table.get("name") or ""):
208
+ raise LakehouseDescriptorError(f"{source}: domain {domain.get('name')!r}: silver_tables.tables[{index}].name must be an identifier")
209
+ if table["name"] in seen:
210
+ raise LakehouseDescriptorError(f"{source}: domain {domain.get('name')!r}: silver_tables.tables must not contain duplicate names")
211
+ seen.add(table["name"])
212
+ if not _IDENTIFIER_PATTERN.fullmatch(table.get("source") or "") or not _IDENTIFIER_PATTERN.fullmatch(table.get("resource") or ""):
213
+ raise LakehouseDescriptorError(f"{source}: domain {domain.get('name')!r}: silver_tables.tables[{index}] must declare source and resource identifiers")
214
+ if "description" in table and not isinstance(table["description"], str):
215
+ raise LakehouseDescriptorError(f"{source}: domain {domain.get('name')!r}: silver_tables.tables[{index}].description must be a string")
216
+ unexpected = set(table) - {"name", "source", "resource", "description"}
217
+ if unexpected:
218
+ raise LakehouseDescriptorError(f"{source}: domain {domain.get('name')!r}: silver_tables.tables[{index}] must not contain {sorted(unexpected)!r}")
219
+
220
+
221
+ def _validate_dashboard(dashboard: Mapping[str, Any], *, source: str) -> None:
222
+ for field in ("name", "products"):
223
+ if field not in dashboard:
224
+ raise LakehouseDescriptorError(f"{source}: dashboard: missing required field {field!r}")
225
+ _identifier(dashboard["name"], field="dashboard.name", source=source)
226
+ products = dashboard["products"]
227
+ if not isinstance(products, list) or not products:
228
+ raise LakehouseDescriptorError(f"{source}: dashboard {dashboard['name']!r}: products must be a non-empty array")
229
+ if any(not isinstance(product, str) or not _IDENTIFIER_PATTERN.fullmatch(product) for product in products):
230
+ raise LakehouseDescriptorError(f"{source}: dashboard {dashboard['name']!r}: products must contain identifiers")
231
+ if len(products) != len(set(products)):
232
+ raise LakehouseDescriptorError(f"{source}: dashboard {dashboard['name']!r}: products must not contain duplicates")
233
+ unexpected = set(dashboard) - {"name", "products"}
234
+ if unexpected:
235
+ raise LakehouseDescriptorError(
236
+ f"{source}: dashboard {dashboard['name']!r} must not contain {sorted(unexpected)!r} "
237
+ "(provider-neutral fields only)"
238
+ )
239
+
240
+
241
+ def validate_lakehouse_descriptor(
242
+ document: Mapping[str, Any], *, source: str = "lakehouse.yaml", allow_incomplete: bool = False
243
+ ) -> None:
244
+ """Validate the v1alpha3 Lakehouse envelope, domains, and dashboards."""
245
+ if document.get("apiVersion") != LAKEHOUSE_API_VERSION:
246
+ raise LakehouseDescriptorError(
247
+ f"{source}: unsupported apiVersion {document.get('apiVersion')!r}; expected {LAKEHOUSE_API_VERSION!r}"
248
+ )
249
+ if document.get("kind") != LAKEHOUSE_KIND:
250
+ raise LakehouseDescriptorError(f"{source}: kind must be {LAKEHOUSE_KIND!r}")
251
+ for field in ("name", "displayName", "description", "status", "sources", "domains", "dashboards"):
252
+ if field not in document:
253
+ raise LakehouseDescriptorError(f"{source}: missing required field {field!r}")
254
+ _identifier(document["name"], field="name", source=source)
255
+ _non_empty_string(document["displayName"], field="displayName", source=source)
256
+ _non_empty_string(document["status"], field="status", source=source)
257
+ _optional_string(document["description"], field="description", source=source)
258
+ sources = document["sources"]
259
+ if not isinstance(sources, list) or (not sources and not allow_incomplete):
260
+ raise LakehouseDescriptorError(f"{source}: sources must be a non-empty array")
261
+ for index, source_name in enumerate(sources):
262
+ _identifier(source_name, field=f"sources[{index}]", source=source)
263
+ if len(sources) != len(set(sources)):
264
+ raise LakehouseDescriptorError(f"{source}: sources must not contain duplicates")
265
+ domains = document["domains"]
266
+ if not isinstance(domains, list) or (not domains and not allow_incomplete):
267
+ raise LakehouseDescriptorError(f"{source}: domains must be a non-empty array")
268
+ seen_domains: set[str] = set()
269
+ seen_products: set[str] = set()
270
+ for domain in domains:
271
+ if not isinstance(domain, Mapping):
272
+ raise LakehouseDescriptorError(f"{source}: domains must contain objects")
273
+ _validate_domain(domain, source=source)
274
+ if domain["name"] in seen_domains:
275
+ raise LakehouseDescriptorError(f"{source}: duplicate domain name {domain['name']!r}")
276
+ seen_domains.add(domain["name"])
277
+ for product in domain["products"]:
278
+ if product["id"] in seen_products:
279
+ raise LakehouseDescriptorError(
280
+ f"{source}: product id {product['id']!r} must be globally unique across domains"
281
+ )
282
+ seen_products.add(product["id"])
283
+ if not seen_products and not allow_incomplete:
284
+ raise LakehouseDescriptorError(
285
+ f"{source}: lakehouse must declare at least one product across its domains "
286
+ "(an individual domain may have zero products while it is being seeded, "
287
+ "but the lakehouse as a whole cannot -- deployment tooling requires a "
288
+ "default product)"
289
+ )
290
+ dashboards = document["dashboards"]
291
+ if not isinstance(dashboards, list):
292
+ raise LakehouseDescriptorError(f"{source}: dashboards must be an array")
293
+ seen_dashboards: set[str] = set()
294
+ for dashboard in dashboards:
295
+ if not isinstance(dashboard, Mapping):
296
+ raise LakehouseDescriptorError(f"{source}: dashboards must contain objects")
297
+ _validate_dashboard(dashboard, source=source)
298
+ if dashboard["name"] in seen_dashboards:
299
+ raise LakehouseDescriptorError(f"{source}: duplicate dashboard name {dashboard['name']!r}")
300
+ seen_dashboards.add(dashboard["name"])
301
+ unknown_products = sorted(set(dashboard["products"]) - seen_products)
302
+ if unknown_products:
303
+ raise LakehouseDescriptorError(
304
+ f"{source}: dashboard {dashboard['name']!r}: products reference unknown product ids "
305
+ f"{unknown_products!r}"
306
+ )
307
+ unexpected = set(document) - {
308
+ "apiVersion", "kind", "name", "displayName", "description", "status", "sources", "domains", "dashboards",
309
+ }
310
+ if unexpected:
311
+ raise LakehouseDescriptorError(
312
+ f"{source}: must not contain {sorted(unexpected)!r} (provider-neutral fields only)"
313
+ )
314
+
315
+
316
+ def load_lakehouse_descriptor(path: str | Path, *, allow_incomplete: bool = False) -> dict[str, Any]:
317
+ """Load and validate the v1alpha3 Lakehouse descriptor at ``path``."""
318
+ source = str(path)
319
+ with Path(path).open(encoding="utf-8") as handle:
320
+ document = yaml.safe_load(handle)
321
+ if not isinstance(document, dict):
322
+ raise LakehouseDescriptorError(f"{source}: descriptor must contain a YAML object")
323
+ validate_lakehouse_descriptor(document, source=source, allow_incomplete=allow_incomplete)
324
+ return document
325
+
326
+
327
+ def load_source_descriptor(path: str | Path) -> dict[str, Any]:
328
+ """Load and validate the v1alpha3 Source descriptor at ``path``."""
329
+ source = str(path)
330
+ with Path(path).open(encoding="utf-8") as handle:
331
+ document = yaml.safe_load(handle)
332
+ if not isinstance(document, dict):
333
+ raise LakehouseDescriptorError(f"{source}: descriptor must contain a YAML object")
334
+ validate_source_descriptor(document, source=source)
335
+ return document
336
+
@@ -0,0 +1,627 @@
1
+ """Typed, provider-neutral inventory derived from lakehouse descriptors.
2
+
3
+ The canonical v1alpha3 inventory is composed from the ``Lakehouse`` descriptor
4
+ at ``lakehouse_code/lakehouse.yaml`` and one ``Source`` descriptor per bronze
5
+ source at ``lakehouse_code/bronze/<source>/source.yaml``.
6
+
7
+ Canonical identities:
8
+
9
+ * Bronze ``<source>.<resource>``
10
+ * Silver ``<domain>.<table>``
11
+ * Gold ``<product>.<table>``
12
+
13
+ with the dependency chain Source/Bronze -> Domain/Silver -> Product/Gold ->
14
+ Dashboard. The legacy v1alpha1/v1alpha2 ``Domain`` inventory loaders below are
15
+ preserved for migration diagnostics; the canonical loaders are the
16
+ ``load_lakehouse_inventory*`` functions.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from collections.abc import Mapping, Sequence
23
+ from dataclasses import dataclass
24
+ from functools import cache
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ from .descriptors import (
29
+ LakehouseDescriptorError,
30
+ load_lakehouse_descriptor,
31
+ load_source_descriptor,
32
+ )
33
+
34
+ _IDENTIFIER_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$")
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Table:
39
+ """A provider-neutral logical table or source resource."""
40
+
41
+ name: str
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class SourceResource:
46
+ """A source-owned Bronze resource."""
47
+
48
+ source: str
49
+ name: str
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class SilverTable:
54
+ """A domain-owned Silver table and its source resource."""
55
+
56
+ domain: str
57
+ name: str
58
+ source: str
59
+ resource: str
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class GoldTable:
64
+ """A product-owned Gold table."""
65
+
66
+ product: str
67
+ name: str
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class ArtifactPrefixes:
72
+ """Object-store locations derived from an owner's logical identity."""
73
+
74
+ manifest_key: str
75
+ floe_report_prefix: str
76
+ dbt_artifact_prefix: str
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class Source:
81
+ """A bronze source and the resources it owns."""
82
+
83
+ descriptor_path: Path
84
+ name: str
85
+ display_name: str
86
+ resources: tuple[Table, ...]
87
+
88
+ @property
89
+ def bronze_namespace(self) -> str:
90
+ return f"{self.name}_bronze"
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class Product:
95
+ """A v1alpha3 product and its provider-neutral derived identities.
96
+
97
+ Product identity is the product ``id`` (also the product ``name``); it is
98
+ globally unique across the lakehouse inventory. The Silver layer is
99
+ domain-aligned (``{domain}_silver``), the Gold layer is product-aligned
100
+ (``{product}_gold``).
101
+ """
102
+
103
+ domain_name: str
104
+ id: str
105
+ display_name: str
106
+ silver_inputs: tuple[str, ...]
107
+ gold_tables: tuple[Table, ...]
108
+
109
+ @property
110
+ def name(self) -> str:
111
+ return self.id
112
+
113
+ @property
114
+ def job_name(self) -> str:
115
+ return f"{self.id}_pipeline"
116
+
117
+ @property
118
+ def silver_namespace(self) -> str:
119
+ return f"{self.domain_name}_silver"
120
+
121
+ @property
122
+ def gold_namespace(self) -> str:
123
+ return f"{self.id}_gold"
124
+
125
+ @property
126
+ def definitions_module(self) -> str:
127
+ return f"lakehouse_code.pipelines.dagster.{self.id}"
128
+
129
+ @property
130
+ def dbt_artifact_prefix(self) -> str:
131
+ return f"run-artifacts/dbt/{self.domain_name}/{self.id}/"
132
+
133
+ @property
134
+ def dbt_project_dir(self) -> str:
135
+ return f"lakehouse_code/gold/{self.id}/dbt"
136
+
137
+ @property
138
+ def openmetadata_data_product_fqns(self) -> tuple[str, str]:
139
+ return self.id, f"{self.domain_name}.{self.id}"
140
+
141
+ @property
142
+ def gold_mart_names(self) -> tuple[str, ...]:
143
+ return tuple(f"{self.gold_namespace}.{table.name}" for table in self.gold_tables)
144
+
145
+
146
+ @dataclass(frozen=True)
147
+ class Domain:
148
+ """A v1alpha3 domain and its products."""
149
+
150
+ descriptor_path: Path
151
+ name: str
152
+ display_name: str
153
+ silver_tables: tuple[SilverTable, ...]
154
+ products: tuple[Product, ...]
155
+
156
+ @property
157
+ def artifact_prefixes(self) -> ArtifactPrefixes:
158
+ return ArtifactPrefixes(
159
+ manifest_key=f"floe/manifests/{self.name}/{self.name}.manifest.json",
160
+ floe_report_prefix=f"floe/reports/{self.name}/",
161
+ dbt_artifact_prefix="",
162
+ )
163
+
164
+ @property
165
+ def silver_namespace(self) -> str:
166
+ return f"{self.name}_silver"
167
+
168
+
169
+ @dataclass(frozen=True)
170
+ class Dashboard:
171
+ """A consumption-aligned dashboard bound to one or more products."""
172
+
173
+ name: str
174
+ products: tuple[str, ...]
175
+
176
+ @property
177
+ def report_source_dir(self) -> str:
178
+ return f"lakehouse_code/dashboards/superset/{self.name}"
179
+
180
+ @property
181
+ def superset_export_bundle_name(self) -> str:
182
+ return f"{self.name}_superset_assets_export.zip"
183
+
184
+
185
+ @dataclass(frozen=True)
186
+ class CatalogNamespace:
187
+ """A physical catalog namespace and its object-store location."""
188
+
189
+ name: str
190
+ location: str
191
+
192
+
193
+ @dataclass(frozen=True)
194
+ class PhysicalProductNames:
195
+ """Provider-specific names resolved for one logical product."""
196
+
197
+ product: str
198
+ gold_namespace: str
199
+ gold_schema_fqn: str
200
+
201
+
202
+ @dataclass(frozen=True)
203
+ class PhysicalDomainNames:
204
+ """Provider-specific names resolved for one logical Silver domain."""
205
+
206
+ domain: str
207
+ silver_namespace: str
208
+ silver_schema_fqn: str
209
+ manifest_uri: str
210
+
211
+
212
+ @dataclass(frozen=True)
213
+ class PhysicalSourceNames:
214
+ """Provider-specific names resolved for one logical bronze source."""
215
+
216
+ source: str
217
+ bronze_namespace: str
218
+ bronze_schema_fqn: str
219
+
220
+
221
+ @dataclass(frozen=True)
222
+ class PhysicalInventory:
223
+ """Physical catalog and artifact identities for a provider contract."""
224
+
225
+ catalog_namespaces: tuple[CatalogNamespace, ...]
226
+ products: tuple[PhysicalProductNames, ...]
227
+ domains: tuple[PhysicalDomainNames, ...] = ()
228
+ sources: tuple[PhysicalSourceNames, ...] = ()
229
+
230
+ @property
231
+ def silver_namespaces(self) -> dict[str, str]:
232
+ return {domain.domain: domain.silver_namespace for domain in self.domains}
233
+
234
+ @property
235
+ def gold_namespaces(self) -> dict[str, str]:
236
+ return {product.product: product.gold_namespace for product in self.products}
237
+
238
+ @property
239
+ def silver_schema_fqns(self) -> dict[str, str]:
240
+ return {domain.domain: domain.silver_schema_fqn for domain in self.domains}
241
+
242
+ @property
243
+ def gold_schema_fqns(self) -> dict[str, str]:
244
+ return {product.product: product.gold_schema_fqn for product in self.products}
245
+
246
+ @property
247
+ def bronze_namespaces(self) -> dict[str, str]:
248
+ return {source.source: source.bronze_namespace for source in self.sources}
249
+
250
+ @property
251
+ def bronze_schema_fqns(self) -> dict[str, str]:
252
+ return {source.source: source.bronze_schema_fqn for source in self.sources}
253
+
254
+ @property
255
+ def domain_floe_manifest_uris(self) -> dict[str, str]:
256
+ return {domain.domain: domain.manifest_uri for domain in self.domains}
257
+
258
+
259
+ @dataclass(frozen=True)
260
+ class LakehouseInventory:
261
+ """All sources, domains, products, and dashboards declared in one lakehouse."""
262
+
263
+ lakehouse_root: Path
264
+ name: str
265
+ sources: tuple[Source, ...]
266
+ domains: tuple[Domain, ...]
267
+ dashboards: tuple[Dashboard, ...]
268
+
269
+ @property
270
+ def products(self) -> tuple[Product, ...]:
271
+ return tuple(product for domain in self.domains for product in domain.products)
272
+
273
+ @property
274
+ def job_names(self) -> tuple[str, ...]:
275
+ return tuple(product.job_name for product in self.products)
276
+
277
+ @property
278
+ def catalog_namespaces(self) -> tuple[str, ...]:
279
+ return tuple(
280
+ namespace
281
+ for namespace in (*self.bronze_namespace_names, *self.silver_namespace_names, *self.gold_namespace_names)
282
+ )
283
+
284
+ @property
285
+ def default_product(self) -> Product:
286
+ if not self.products:
287
+ raise LakehouseDescriptorError(f"{self.lakehouse_root}: no data products were discovered")
288
+ return self.products[0]
289
+
290
+ @property
291
+ def artifact_prefixes(self) -> tuple[ArtifactPrefixes, ...]:
292
+ return tuple(domain.artifact_prefixes for domain in self.domains)
293
+
294
+ @property
295
+ def domain_names(self) -> tuple[str, ...]:
296
+ return tuple(domain.name for domain in self.domains)
297
+
298
+ @property
299
+ def source_names(self) -> tuple[str, ...]:
300
+ return tuple(source.name for source in self.sources)
301
+
302
+ @property
303
+ def silver_table_count(self) -> int:
304
+ """Count of distinct product-reachable physical Silver relations.
305
+
306
+ A domain may declare Silver tables ahead of any product consuming
307
+ them (see the product-less-domain scaffolding path), so this counts
308
+ only tables referenced by at least one product's ``silver_inputs``,
309
+ not every declared table. Two products in the same domain can
310
+ declare the same table (they share the domain's Silver namespace),
311
+ so this counts unique ``(domain, table)`` pairs rather than summing
312
+ per-product declarations, which would double-count a shared table.
313
+ """
314
+ return sum(
315
+ len({name for product in domain.products for name in product.silver_inputs})
316
+ for domain in self.domains
317
+ )
318
+
319
+ @property
320
+ def gold_table_count(self) -> int:
321
+ return sum(len(product.gold_tables) for product in self.products)
322
+
323
+ @property
324
+ def gold_mart_names(self) -> tuple[str, ...]:
325
+ return tuple(mart for product in self.products for mart in product.gold_mart_names)
326
+
327
+ @property
328
+ def manifest_keys(self) -> tuple[str, ...]:
329
+ return tuple(domain.artifact_prefixes.manifest_key for domain in self.domains)
330
+
331
+ @property
332
+ def openmetadata_data_products(self) -> dict[str, tuple[str, str]]:
333
+ return {product.name: product.openmetadata_data_product_fqns for product in self.products}
334
+
335
+ @property
336
+ def bronze_namespace_names(self) -> frozenset[str]:
337
+ return frozenset(source.bronze_namespace for source in self.sources)
338
+
339
+ @property
340
+ def silver_namespace_names(self) -> frozenset[str]:
341
+ return frozenset(domain.silver_namespace for domain in self.domains)
342
+
343
+ def domain_for_product(self, product: Product) -> Domain:
344
+ return next(domain for domain in self.domains if domain.name == product.domain_name)
345
+
346
+ def resolved_silver_tables(self, product: Product) -> tuple[SilverTable, ...]:
347
+ domain = self.domain_for_product(product)
348
+ by_name = {table.name: table for table in domain.silver_tables}
349
+ return tuple(by_name[name] for name in product.silver_inputs)
350
+
351
+ def bronze_resources_for_product(self, product: Product) -> tuple[SourceResource, ...]:
352
+ return tuple(SourceResource(table.source, table.resource) for table in self.resolved_silver_tables(product))
353
+
354
+ @property
355
+ def gold_namespace_names(self) -> frozenset[str]:
356
+ return frozenset(product.gold_namespace for product in self.products)
357
+
358
+ def resolve_physical_names(
359
+ self,
360
+ *,
361
+ catalog_database_fqn: str,
362
+ bronze_bucket: str,
363
+ silver_bucket: str,
364
+ gold_bucket: str,
365
+ manifest_base_uri: str,
366
+ ) -> PhysicalInventory:
367
+ """Resolve physical names from provider-contract values."""
368
+ namespaces: list[CatalogNamespace] = []
369
+ products: list[PhysicalProductNames] = []
370
+ domains: list[PhysicalDomainNames] = []
371
+ sources: list[PhysicalSourceNames] = []
372
+ for domain in self.domains:
373
+ namespaces.append(
374
+ CatalogNamespace(domain.silver_namespace, f"s3://{silver_bucket}/{domain.silver_namespace}/")
375
+ )
376
+ domains.append(
377
+ PhysicalDomainNames(
378
+ domain=domain.name,
379
+ silver_namespace=domain.silver_namespace,
380
+ silver_schema_fqn=f"{catalog_database_fqn}.{domain.silver_namespace}",
381
+ manifest_uri=f"{manifest_base_uri.rstrip('/')}/{domain.name}/{domain.name}.manifest.json",
382
+ )
383
+ )
384
+ for product in self.products:
385
+ namespaces.append(
386
+ CatalogNamespace(product.gold_namespace, f"s3://{gold_bucket}/{product.gold_namespace}/")
387
+ )
388
+ products.append(
389
+ PhysicalProductNames(
390
+ product=product.name,
391
+ gold_namespace=product.gold_namespace,
392
+ gold_schema_fqn=f"{catalog_database_fqn}.{product.gold_namespace}",
393
+ )
394
+ )
395
+ for source in self.sources:
396
+ namespaces.append(
397
+ CatalogNamespace(source.bronze_namespace, f"s3://{bronze_bucket}/{source.bronze_namespace}/")
398
+ )
399
+ sources.append(
400
+ PhysicalSourceNames(
401
+ source=source.name,
402
+ bronze_namespace=source.bronze_namespace,
403
+ bronze_schema_fqn=f"{catalog_database_fqn}.{source.bronze_namespace}",
404
+ )
405
+ )
406
+ return PhysicalInventory(
407
+ catalog_namespaces=tuple(namespaces), products=tuple(products), domains=tuple(domains), sources=tuple(sources)
408
+ )
409
+
410
+
411
+ def _lakehouse_root(path: Path) -> Path:
412
+ candidate = path.resolve()
413
+ return candidate / "lakehouse_code" if (candidate / "lakehouse_code").is_dir() else candidate
414
+
415
+
416
+ def _source_table_names(source: Mapping[str, Any]) -> set[str]:
417
+ resources = source.get("resources")
418
+ if not isinstance(resources, list):
419
+ return set()
420
+ return {resource["name"] for resource in resources if isinstance(resource, Mapping) and "name" in resource}
421
+
422
+
423
+ def _product(
424
+ path: Path, domain_name: str, product: Mapping[str, Any], index: int, silver_tables: tuple[SilverTable, ...]
425
+ ) -> Product:
426
+ label = product.get("id") or f"products[{index}]"
427
+ product_id = product.get("id")
428
+ if not isinstance(product_id, str) or not _IDENTIFIER_PATTERN.fullmatch(product_id):
429
+ raise LakehouseDescriptorError(f"{path}: product {label!r}: id must match '^[a-z][a-z0-9_]*$'")
430
+ display_name = product.get("displayName")
431
+ if not isinstance(display_name, str) or not display_name:
432
+ raise LakehouseDescriptorError(f"{path}: product {label!r}: displayName is required and must be a non-empty string")
433
+ inputs = product.get("silver_inputs")
434
+ if not isinstance(inputs, list) or not inputs:
435
+ raise LakehouseDescriptorError(f"{path}: product {label!r}: silver_inputs must be a non-empty array")
436
+ return Product(
437
+ domain_name=domain_name,
438
+ id=product_id,
439
+ display_name=display_name,
440
+ silver_inputs=tuple(inputs),
441
+ gold_tables=_table_group(path, product, "gold_tables", label),
442
+ )
443
+
444
+
445
+ def _silver_tables(path: Path, domain_name: str, domain: Mapping[str, Any]) -> tuple[SilverTable, ...]:
446
+ tables = domain["silver_tables"]["tables"]
447
+ return tuple(
448
+ SilverTable(domain=domain_name, name=table["name"], source=table["source"], resource=table["resource"])
449
+ for table in tables
450
+ )
451
+
452
+
453
+ def _table_group(path: Path, product: Mapping[str, Any], field: str, label: str) -> tuple[Table, ...]:
454
+ spec = product.get(field)
455
+ if not isinstance(spec, Mapping):
456
+ raise LakehouseDescriptorError(f"{path}: product {label!r}: {field} is required and must be an object")
457
+ tables = spec.get("tables")
458
+ if not isinstance(tables, list) or not tables:
459
+ raise LakehouseDescriptorError(f"{path}: product {label!r}: {field}.tables must be a non-empty array")
460
+ names = [table.get("name") for table in tables if isinstance(table, Mapping)]
461
+ if len(names) != len(tables) or any(not isinstance(name, str) or not name for name in names):
462
+ raise LakehouseDescriptorError(f"{path}: product {label!r}: {field}.tables must contain non-empty string names")
463
+ if len(names) != len(set(names)):
464
+ raise LakehouseDescriptorError(f"{path}: product {label!r}: {field}.tables must not contain duplicate names")
465
+ return tuple(Table(name=name) for name in names)
466
+
467
+
468
+ def _validate_inventory_identities(
469
+ lakehouse_root: Path,
470
+ sources: tuple[Source, ...],
471
+ domains: tuple[Domain, ...],
472
+ dashboards: tuple[Dashboard, ...],
473
+ ) -> None:
474
+ seen_sources: set[str] = set()
475
+ for source in sources:
476
+ if source.name in seen_sources:
477
+ raise LakehouseDescriptorError(f"{source.descriptor_path}: duplicate source name {source.name!r}")
478
+ seen_sources.add(source.name)
479
+ seen_domains: set[str] = set()
480
+ seen_product_ids: set[str] = set()
481
+ for domain in domains:
482
+ if domain.name in seen_domains:
483
+ raise LakehouseDescriptorError(f"{domain.descriptor_path}: duplicate domain name {domain.name!r}")
484
+ seen_domains.add(domain.name)
485
+ silver_by_name = {table.name: table for table in domain.silver_tables}
486
+ for table in domain.silver_tables:
487
+ source = next((candidate for candidate in sources if candidate.name == table.source), None)
488
+ if source is None:
489
+ raise LakehouseDescriptorError(
490
+ f"{domain.descriptor_path}: domain {domain.name!r}: Silver table {table.name!r} references unknown source {table.source!r}"
491
+ )
492
+ if table.resource not in {resource.name for resource in source.resources}:
493
+ raise LakehouseDescriptorError(
494
+ f"{domain.descriptor_path}: domain {domain.name!r}: Silver table {table.name!r} references unknown "
495
+ f"resource {table.resource!r} of source {table.source!r}"
496
+ )
497
+ for product in domain.products:
498
+ if product.id in seen_product_ids:
499
+ raise LakehouseDescriptorError(
500
+ f"{domain.descriptor_path}: product {product.id!r}: id must be globally unique across the lakehouse"
501
+ )
502
+ seen_product_ids.add(product.id)
503
+ unknown = [name for name in product.silver_inputs if name not in silver_by_name]
504
+ if unknown:
505
+ raise LakehouseDescriptorError(
506
+ f"{domain.descriptor_path}: product {product.id!r}: silver_inputs reference unknown domain Silver tables {unknown!r}"
507
+ )
508
+ seen_dashboards: set[str] = set()
509
+ for dashboard in dashboards:
510
+ if dashboard.name in seen_dashboards:
511
+ raise LakehouseDescriptorError(f"{lakehouse_root}: duplicate dashboard name {dashboard.name!r}")
512
+ seen_dashboards.add(dashboard.name)
513
+ unknown_products = sorted(set(dashboard.products) - seen_product_ids)
514
+ if unknown_products:
515
+ raise LakehouseDescriptorError(
516
+ f"{lakehouse_root}: dashboard {dashboard.name!r}: products {unknown_products!r} must reference declared products"
517
+ )
518
+
519
+
520
+ def load_lakehouse_inventory_from_descriptors(
521
+ lakehouse_path: str | Path,
522
+ source_paths: Sequence[str | Path],
523
+ *,
524
+ source_label: str | Path | None = None,
525
+ allow_incomplete: bool = False,
526
+ ) -> LakehouseInventory:
527
+ """Load and validate one Lakehouse descriptor plus its Source descriptors."""
528
+ lakehouse_document = load_lakehouse_descriptor(lakehouse_path, allow_incomplete=allow_incomplete)
529
+ sources: list[Source] = []
530
+ for source_path in source_paths:
531
+ document = load_source_descriptor(source_path)
532
+ name = document["name"]
533
+ if name != Path(source_path).parent.name:
534
+ raise LakehouseDescriptorError(
535
+ f"{source_path}: name {name!r} must match source directory {Path(source_path).parent.name!r}"
536
+ )
537
+ resources = tuple(
538
+ Table(name=resource["name"])
539
+ for resource in document["resources"]
540
+ if isinstance(resource, Mapping) and "name" in resource
541
+ )
542
+ sources.append(
543
+ Source(
544
+ descriptor_path=Path(source_path),
545
+ name=name,
546
+ display_name=document["displayName"],
547
+ resources=resources,
548
+ )
549
+ )
550
+ declared_sources = set(lakehouse_document["sources"])
551
+ discovered_sources = {source.name for source in sources}
552
+ if declared_sources != discovered_sources:
553
+ raise LakehouseDescriptorError(
554
+ f"{lakehouse_path}: lakehouse sources {sorted(declared_sources)!r} must match discovered "
555
+ f"source descriptors {sorted(discovered_sources)!r} under lakehouse_code/bronze/*/source.yaml"
556
+ )
557
+ domains: list[Domain] = []
558
+ for index, domain in enumerate(lakehouse_document["domains"]):
559
+ if not isinstance(domain, Mapping):
560
+ raise LakehouseDescriptorError(f"{lakehouse_path}: domains[{index}] must be an object")
561
+ domain_name = domain["name"]
562
+ silver_tables = _silver_tables(lakehouse_path, domain_name, domain)
563
+ products = tuple(
564
+ _product(lakehouse_path, domain_name, product, product_index, silver_tables)
565
+ for product_index, product in enumerate(domain["products"])
566
+ if isinstance(product, Mapping)
567
+ )
568
+ domains.append(
569
+ Domain(
570
+ descriptor_path=Path(lakehouse_path),
571
+ name=domain_name,
572
+ display_name=domain["displayName"],
573
+ silver_tables=silver_tables,
574
+ products=products,
575
+ )
576
+ )
577
+ dashboards = tuple(
578
+ Dashboard(
579
+ name=dashboard["name"],
580
+ products=tuple(dashboard["products"]),
581
+ )
582
+ for dashboard in lakehouse_document["dashboards"]
583
+ if isinstance(dashboard, Mapping)
584
+ )
585
+ inventory = LakehouseInventory(
586
+ lakehouse_root=Path(source_label) if source_label is not None else Path(lakehouse_path).parent,
587
+ name=lakehouse_document["name"],
588
+ sources=tuple(sources),
589
+ domains=tuple(domains),
590
+ dashboards=dashboards,
591
+ )
592
+ _validate_inventory_identities(inventory.lakehouse_root, inventory.sources, inventory.domains, inventory.dashboards)
593
+ return inventory
594
+
595
+
596
+ def load_lakehouse_inventory(path: str | Path, *, allow_incomplete: bool = False) -> LakehouseInventory:
597
+ """Load and validate ``lakehouse_code/lakehouse.yaml`` plus every source."""
598
+ lakehouse_root = _lakehouse_root(Path(path))
599
+ lakehouse_descriptor = lakehouse_root / "lakehouse.yaml"
600
+ if not lakehouse_descriptor.is_file():
601
+ raise LakehouseDescriptorError(f"{lakehouse_root}: no lakehouse descriptor found at lakehouse.yaml")
602
+ source_descriptors = sorted(lakehouse_root.glob("bronze/*/source.yaml"))
603
+ if not source_descriptors and not allow_incomplete:
604
+ raise LakehouseDescriptorError(f"{lakehouse_root}: no source descriptors found at bronze/*/source.yaml")
605
+ return load_lakehouse_inventory_from_descriptors(
606
+ lakehouse_descriptor, source_descriptors, source_label=lakehouse_root, allow_incomplete=allow_incomplete
607
+ )
608
+
609
+
610
+ def load_transitional_lakehouse_inventory(path: str | Path) -> LakehouseInventory:
611
+ """Load an `olf init --empty` project while it has no runnable product.
612
+
613
+ This is intentionally limited to scaffold planning. Deployment and
614
+ ordinary descriptor validation retain the strict v1alpha3 contract.
615
+ """
616
+ return load_lakehouse_inventory(path, allow_incomplete=True)
617
+
618
+
619
+ @cache
620
+ def _cached_lakehouse_inventory(resolved_path: str) -> LakehouseInventory:
621
+ return load_lakehouse_inventory(resolved_path)
622
+
623
+
624
+ def inventory_for(repo_root: str | Path) -> LakehouseInventory:
625
+ """Load and cache the canonical lakehouse inventory for a repository root."""
626
+ return _cached_lakehouse_inventory(str(Path(repo_root).resolve()))
627
+
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: openlakeforge-domain-model
3
+ Version: 0.2.0a1
4
+ Summary: Canonical provider-neutral domain model for OpenLakeForge
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: PyYAML<7,>=6.0
@@ -0,0 +1,9 @@
1
+ pyproject.toml
2
+ openlakeforge_domain/__init__.py
3
+ openlakeforge_domain/descriptors.py
4
+ openlakeforge_domain/inventory.py
5
+ openlakeforge_domain_model.egg-info/PKG-INFO
6
+ openlakeforge_domain_model.egg-info/SOURCES.txt
7
+ openlakeforge_domain_model.egg-info/dependency_links.txt
8
+ openlakeforge_domain_model.egg-info/requires.txt
9
+ openlakeforge_domain_model.egg-info/top_level.txt
@@ -0,0 +1,13 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "openlakeforge-domain-model"
7
+ version = "0.2.0a1"
8
+ description = "Canonical provider-neutral domain model for OpenLakeForge"
9
+ requires-python = ">=3.12"
10
+ dependencies = ["PyYAML>=6.0,<7"]
11
+
12
+ [tool.setuptools.packages.find]
13
+ include = ["openlakeforge_domain*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+