dq-export-connector 0.1.0.dev1__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.
File without changes
File without changes
@@ -0,0 +1,63 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: bucket_validation_result.json
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Annotated, Literal
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, RootModel
9
+
10
+
11
+ class BucketValidationResult1(BaseModel):
12
+ model_config = ConfigDict(
13
+ populate_by_name=True,
14
+ )
15
+ status: Literal['OK']
16
+
17
+
18
+ class ExportMessage(BaseModel):
19
+ model_config = ConfigDict(
20
+ populate_by_name=True,
21
+ )
22
+ code: Annotated[
23
+ str,
24
+ Field(
25
+ description='SCREAMING_SNAKE_CASE, such as `ACCESS_DENIED`. A string rather than an\nenum, so a worker release can add codes that readers built against an\nolder ddc still accept.'
26
+ ),
27
+ ]
28
+ message: Annotated[
29
+ str,
30
+ Field(
31
+ description='Safe to show to the user: no credentials and no dataset values.'
32
+ ),
33
+ ]
34
+
35
+
36
+ class BucketValidationResult2(BaseModel):
37
+ model_config = ConfigDict(
38
+ populate_by_name=True,
39
+ )
40
+ status: Literal['OK_WITH_WARNINGS']
41
+ warnings: list[ExportMessage]
42
+
43
+
44
+ class BucketValidationResult3(BaseModel):
45
+ model_config = ConfigDict(
46
+ populate_by_name=True,
47
+ )
48
+ errors: list[ExportMessage]
49
+ status: Literal['ERROR']
50
+
51
+
52
+ class BucketValidationResult(
53
+ RootModel[
54
+ BucketValidationResult1 | BucketValidationResult2 | BucketValidationResult3
55
+ ]
56
+ ):
57
+ root: Annotated[
58
+ BucketValidationResult1 | BucketValidationResult2 | BucketValidationResult3,
59
+ Field(
60
+ description='The result of a Bucket `ValidateConnection`. Connection problems are\nreported here, and the task still succeeds.',
61
+ title='BucketValidationResult',
62
+ ),
63
+ ]
@@ -0,0 +1,84 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: export_connector_credentials.json
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Annotated
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, RootModel
9
+
10
+
11
+ class S3(BaseModel):
12
+ model_config = ConfigDict(
13
+ populate_by_name=True,
14
+ )
15
+ access_key: Annotated[str, Field(alias='accessKey')]
16
+ secret_key: Annotated[str, Field(alias='secretKey')]
17
+
18
+
19
+ class BucketCredentials1(BaseModel):
20
+ model_config = ConfigDict(
21
+ extra='forbid',
22
+ populate_by_name=True,
23
+ )
24
+ s3: S3
25
+
26
+
27
+ class AzureBlob(BaseModel):
28
+ model_config = ConfigDict(
29
+ populate_by_name=True,
30
+ )
31
+ sas_token: Annotated[
32
+ str,
33
+ Field(
34
+ alias='sasToken',
35
+ description='A container SAS token — the scoped, expiring grant Azure recommends\nover an account key. Only the grant: the account and container it\napplies to are pinned by the signed `BucketConnection`, so rotating\nthe token can never repoint the connector.',
36
+ ),
37
+ ]
38
+
39
+
40
+ class BucketCredentials2(BaseModel):
41
+ model_config = ConfigDict(
42
+ extra='forbid',
43
+ populate_by_name=True,
44
+ )
45
+ azure_blob: Annotated[AzureBlob, Field(alias='azureBlob')]
46
+
47
+
48
+ class Gcs(BaseModel):
49
+ model_config = ConfigDict(
50
+ populate_by_name=True,
51
+ )
52
+ service_account_json: Annotated[str, Field(alias='serviceAccountJson')]
53
+
54
+
55
+ class BucketCredentials3(BaseModel):
56
+ model_config = ConfigDict(
57
+ extra='forbid',
58
+ populate_by_name=True,
59
+ )
60
+ gcs: Gcs
61
+
62
+
63
+ class BucketCredentials(
64
+ RootModel[BucketCredentials1 | BucketCredentials2 | BucketCredentials3]
65
+ ):
66
+ root: BucketCredentials1 | BucketCredentials2 | BucketCredentials3
67
+
68
+
69
+ class ExportConnectorCredentials1(BaseModel):
70
+ model_config = ConfigDict(
71
+ extra='forbid',
72
+ populate_by_name=True,
73
+ )
74
+ bucket: BucketCredentials
75
+
76
+
77
+ class ExportConnectorCredentials(RootModel[ExportConnectorCredentials1]):
78
+ root: Annotated[
79
+ ExportConnectorCredentials1,
80
+ Field(
81
+ description='Keyed by Connector Kind, in step with [`ExportConnectorKind`]. The worker\nreads it at `/input/credentials.json`.',
82
+ title='ExportConnectorCredentials',
83
+ ),
84
+ ]
@@ -0,0 +1,152 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: export_receipt.json
3
+
4
+ from __future__ import annotations
5
+
6
+ from enum import Enum
7
+ from typing import Annotated, Literal
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, RootModel
10
+
11
+
12
+ class ExportMessage(BaseModel):
13
+ model_config = ConfigDict(
14
+ populate_by_name=True,
15
+ )
16
+ code: Annotated[
17
+ str,
18
+ Field(
19
+ description='SCREAMING_SNAKE_CASE, such as `ACCESS_DENIED`. A string rather than an\nenum, so a worker release can add codes that readers built against an\nolder ddc still accept.'
20
+ ),
21
+ ]
22
+ message: Annotated[
23
+ str,
24
+ Field(
25
+ description='Safe to show to the user: no credentials and no dataset values.'
26
+ ),
27
+ ]
28
+
29
+
30
+ class ExportTotals(BaseModel):
31
+ model_config = ConfigDict(
32
+ populate_by_name=True,
33
+ )
34
+ bytes: Annotated[int, Field(ge=0)]
35
+ files: Annotated[int, Field(ge=0)]
36
+ rows: Annotated[
37
+ int | None,
38
+ Field(
39
+ description='`None` unless the worker counted the rows of every file.', ge=0
40
+ ),
41
+ ] = None
42
+
43
+
44
+ class ExportedFile(BaseModel):
45
+ model_config = ConfigDict(
46
+ populate_by_name=True,
47
+ )
48
+ bytes: Annotated[int, Field(ge=0)]
49
+ path: Annotated[str, Field(description='The object key in the bucket.')]
50
+ rows: Annotated[
51
+ int | None, Field(description="`None` where the worker can't count rows.", ge=0)
52
+ ] = None
53
+
54
+
55
+ class InputStatus(Enum):
56
+ success = 'SUCCESS'
57
+ failed = 'FAILED'
58
+
59
+
60
+ class BucketInputReceipt(BaseModel):
61
+ model_config = ConfigDict(
62
+ populate_by_name=True,
63
+ )
64
+ errors: list[ExportMessage]
65
+ files: list[ExportedFile]
66
+ leaf_id: Annotated[str, Field(alias='leafId')]
67
+ status: InputStatus
68
+ totals: ExportTotals
69
+ warnings: list[ExportMessage]
70
+
71
+
72
+ class ExportReceiptV01(BaseModel):
73
+ model_config = ConfigDict(
74
+ populate_by_name=True,
75
+ )
76
+ errors: Annotated[
77
+ list[ExportMessage],
78
+ Field(
79
+ description='Problems tied to no single Input, such as credentials rejected before\nany upload.'
80
+ ),
81
+ ]
82
+ export_prefix: Annotated[
83
+ str | None,
84
+ Field(
85
+ alias='exportPrefix',
86
+ description='The export prefix with its date tokens filled; `None` without one.',
87
+ ),
88
+ ] = None
89
+ inputs: list[BucketInputReceipt]
90
+ totals: ExportTotals
91
+ warnings: list[ExportMessage]
92
+ type: Literal['BUCKET']
93
+
94
+
95
+ class ExportReceiptV0(RootModel[ExportReceiptV01]):
96
+ root: ExportReceiptV01
97
+
98
+
99
+ class ExportReceipt1(BaseModel):
100
+ model_config = ConfigDict(
101
+ populate_by_name=True,
102
+ )
103
+ version: Literal['V0']
104
+ errors: Annotated[
105
+ list[ExportMessage],
106
+ Field(
107
+ description='Problems tied to no single Input, such as credentials rejected before\nany upload.'
108
+ ),
109
+ ]
110
+ export_prefix: Annotated[
111
+ str | None,
112
+ Field(
113
+ alias='exportPrefix',
114
+ description='The export prefix with its date tokens filled; `None` without one.',
115
+ ),
116
+ ] = None
117
+ inputs: list[BucketInputReceipt]
118
+ totals: ExportTotals
119
+ warnings: list[ExportMessage]
120
+ type: Literal['BUCKET']
121
+
122
+
123
+ class ExportReceipt(RootModel[ExportReceipt1]):
124
+ root: Annotated[
125
+ ExportReceipt1,
126
+ Field(
127
+ description='A released version never changes: a change is a new version, and readers\nkeep reading every version ever written.',
128
+ title='ExportReceipt',
129
+ ),
130
+ ]
131
+
132
+
133
+ class BucketExportReceiptV0(BaseModel):
134
+ model_config = ConfigDict(
135
+ populate_by_name=True,
136
+ )
137
+ errors: Annotated[
138
+ list[ExportMessage],
139
+ Field(
140
+ description='Problems tied to no single Input, such as credentials rejected before\nany upload.'
141
+ ),
142
+ ]
143
+ export_prefix: Annotated[
144
+ str | None,
145
+ Field(
146
+ alias='exportPrefix',
147
+ description='The export prefix with its date tokens filled; `None` without one.',
148
+ ),
149
+ ] = None
150
+ inputs: list[BucketInputReceipt]
151
+ totals: ExportTotals
152
+ warnings: list[ExportMessage]
@@ -0,0 +1,183 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: export_worker_config.json
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Annotated, Literal
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, RootModel
9
+
10
+
11
+ class BucketConnection1(BaseModel):
12
+ model_config = ConfigDict(
13
+ populate_by_name=True,
14
+ )
15
+ bucket: str
16
+ path_prefix: Annotated[str | None, Field(alias='pathPrefix')] = None
17
+ region: str
18
+ type: Literal['s3']
19
+
20
+
21
+ class BucketConnection2(BaseModel):
22
+ model_config = ConfigDict(
23
+ populate_by_name=True,
24
+ )
25
+ account_name: Annotated[str, Field(alias='accountName')]
26
+ container: str
27
+ path_prefix: Annotated[str | None, Field(alias='pathPrefix')] = None
28
+ type: Literal['azureBlob']
29
+
30
+
31
+ class BucketConnection3(BaseModel):
32
+ model_config = ConfigDict(
33
+ populate_by_name=True,
34
+ )
35
+ bucket: str
36
+ path_prefix: Annotated[str | None, Field(alias='pathPrefix')] = None
37
+ type: Literal['gcs']
38
+
39
+
40
+ class BucketConnection(
41
+ RootModel[BucketConnection1 | BucketConnection2 | BucketConnection3]
42
+ ):
43
+ root: Annotated[
44
+ BucketConnection1 | BucketConnection2 | BucketConnection3,
45
+ Field(
46
+ description='Cloud-bucket addressing (no credentials). Provider-structurally paired with\nthe matching [`BucketCredentials`] variant in the confidential half.'
47
+ ),
48
+ ]
49
+
50
+
51
+ class BucketInput(BaseModel):
52
+ model_config = ConfigDict(
53
+ populate_by_name=True,
54
+ )
55
+ destination_directory: Annotated[str, Field(alias='destinationDirectory')]
56
+ leaf_id: Annotated[str, Field(alias='leafId')]
57
+
58
+
59
+ class ExportConnectorKind1(BaseModel):
60
+ model_config = ConfigDict(
61
+ populate_by_name=True,
62
+ )
63
+ connection: BucketConnection
64
+ export_prefix: Annotated[
65
+ str | None,
66
+ Field(
67
+ alias='exportPrefix',
68
+ description='May hold `{yyyy}`, `{mm}`, `{dd}` and `{hh}`, which the worker fills in\nUTC when a run starts.',
69
+ ),
70
+ ] = None
71
+ inputs: list[BucketInput]
72
+ type: Literal['bucket']
73
+
74
+
75
+ class ExportConnectorKind(RootModel[ExportConnectorKind1]):
76
+ root: ExportConnectorKind1
77
+
78
+
79
+ class ExportParams1(BaseModel):
80
+ model_config = ConfigDict(
81
+ populate_by_name=True,
82
+ )
83
+ type: Literal['bucket']
84
+
85
+
86
+ class ExportParams(RootModel[ExportParams1]):
87
+ root: ExportParams1
88
+
89
+
90
+ class ProvisionedPolicy(BaseModel):
91
+ model_config = ConfigDict(
92
+ populate_by_name=True,
93
+ )
94
+ leaf_id: Annotated[str, Field(alias='leafId')]
95
+ policy_id: Annotated[str, Field(alias='policyId')]
96
+ provisioned_at: Annotated[
97
+ str, Field(alias='provisionedAt', description='RFC 3339.')
98
+ ]
99
+ provisioned_by_email: Annotated[
100
+ str,
101
+ Field(
102
+ alias='provisionedByEmail',
103
+ description='Whose READ on the dataset every run re-checks.',
104
+ ),
105
+ ]
106
+
107
+
108
+ class ValidateConnectionParams1(BaseModel):
109
+ model_config = ConfigDict(
110
+ populate_by_name=True,
111
+ )
112
+ type: Literal['bucket']
113
+
114
+
115
+ class ValidateConnectionParams(RootModel[ValidateConnectionParams1]):
116
+ root: ValidateConnectionParams1
117
+
118
+
119
+ class BucketExportConfig(BaseModel):
120
+ model_config = ConfigDict(
121
+ populate_by_name=True,
122
+ )
123
+ connection: BucketConnection
124
+ export_prefix: Annotated[
125
+ str | None,
126
+ Field(
127
+ alias='exportPrefix',
128
+ description='May hold `{yyyy}`, `{mm}`, `{dd}` and `{hh}`, which the worker fills in\nUTC when a run starts.',
129
+ ),
130
+ ] = None
131
+ inputs: list[BucketInput]
132
+
133
+
134
+ class ValidateConnection(BaseModel):
135
+ model_config = ConfigDict(
136
+ populate_by_name=True,
137
+ )
138
+ params: ValidateConnectionParams
139
+
140
+
141
+ class ExportConnectorDcrComputeAction1(BaseModel):
142
+ model_config = ConfigDict(
143
+ extra='forbid',
144
+ populate_by_name=True,
145
+ )
146
+ validate_connection: Annotated[
147
+ ValidateConnection, Field(alias='validateConnection')
148
+ ]
149
+
150
+
151
+ class Export(BaseModel):
152
+ model_config = ConfigDict(
153
+ populate_by_name=True,
154
+ )
155
+ params: ExportParams
156
+
157
+
158
+ class ExportConnectorDcrComputeAction2(BaseModel):
159
+ model_config = ConfigDict(
160
+ extra='forbid',
161
+ populate_by_name=True,
162
+ )
163
+ export: Export
164
+
165
+
166
+ class ExportConnectorDcrComputeAction(
167
+ RootModel[ExportConnectorDcrComputeAction1 | ExportConnectorDcrComputeAction2]
168
+ ):
169
+ root: Annotated[
170
+ ExportConnectorDcrComputeAction1 | ExportConnectorDcrComputeAction2,
171
+ Field(
172
+ description='Compute actions for an Export Connector DCR, started through `POST /jobs`.'
173
+ ),
174
+ ]
175
+
176
+
177
+ class ExportWorkerConfig(BaseModel):
178
+ model_config = ConfigDict(
179
+ populate_by_name=True,
180
+ )
181
+ action: ExportConnectorDcrComputeAction
182
+ connector: ExportConnectorKind
183
+ policies: list[ProvisionedPolicy]
File without changes
@@ -0,0 +1,71 @@
1
+ """How an Input's dataset maps to files. A ZIP whose entries are all stored
2
+ without compression is unpacked, each entry keeping its path. Anything else,
3
+ including a ZIP with a compressed entry, is one raw file named `data`."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import zipfile
8
+ from collections.abc import Iterator
9
+ from contextlib import contextmanager
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import BinaryIO
13
+
14
+ RAW_FILE_NAME = "data"
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class DatasetFile:
19
+ """`path` is relative to the Input's destination directory."""
20
+
21
+ path: str
22
+ size: int
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Layout:
27
+ """`raw_reason` says why a dataset isn't unpacked; `None` for a stored ZIP."""
28
+
29
+ files: list[DatasetFile]
30
+ raw_reason: str | None
31
+
32
+
33
+ @contextmanager
34
+ def open_dataset(dataset_path: Path) -> Iterator[tuple[Layout, _Reader]]:
35
+ """Reads the dataset's layout, keeping a ZIP open while its entries upload.
36
+ A ZIP's index sits at its end, so detecting one reads no more than that."""
37
+ try:
38
+ archive = zipfile.ZipFile(dataset_path)
39
+ except zipfile.BadZipFile:
40
+ size = dataset_path.stat().st_size
41
+ yield Layout([DatasetFile(RAW_FILE_NAME, size)], "not a ZIP"), _RawReader(dataset_path)
42
+ return
43
+ with archive:
44
+ entries = archive.infolist()
45
+ if any(entry.compress_type != zipfile.ZIP_STORED for entry in entries):
46
+ size = dataset_path.stat().st_size
47
+ yield Layout([DatasetFile(RAW_FILE_NAME, size)], "a ZIP with compressed entries"), _RawReader(dataset_path)
48
+ return
49
+ files = [DatasetFile(entry.filename, entry.file_size) for entry in entries if not entry.is_dir()]
50
+ yield Layout(files, None), _ZipReader(archive)
51
+
52
+
53
+ class _Reader:
54
+ def open(self, file: DatasetFile) -> BinaryIO:
55
+ raise NotImplementedError
56
+
57
+
58
+ class _RawReader(_Reader):
59
+ def __init__(self, dataset_path: Path) -> None:
60
+ self._dataset_path = dataset_path
61
+
62
+ def open(self, file: DatasetFile) -> BinaryIO:
63
+ return self._dataset_path.open("rb")
64
+
65
+
66
+ class _ZipReader(_Reader):
67
+ def __init__(self, archive: zipfile.ZipFile) -> None:
68
+ self._archive = archive
69
+
70
+ def open(self, file: DatasetFile) -> BinaryIO:
71
+ return self._archive.open(file.path) # type: ignore[return-value]