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.
- dq_export_connector/__init__.py +0 -0
- dq_export_connector/_models/__init__.py +0 -0
- dq_export_connector/_models/bucket_validation_result.py +63 -0
- dq_export_connector/_models/export_connector_credentials.py +84 -0
- dq_export_connector/_models/export_receipt.py +152 -0
- dq_export_connector/_models/export_worker_config.py +183 -0
- dq_export_connector/bucket/__init__.py +0 -0
- dq_export_connector/bucket/datasets.py +71 -0
- dq_export_connector/bucket/export.py +215 -0
- dq_export_connector/bucket/paths.py +83 -0
- dq_export_connector/bucket/store.py +179 -0
- dq_export_connector/bucket/upload.py +46 -0
- dq_export_connector/bucket/validate.py +71 -0
- dq_export_connector/log.py +45 -0
- dq_export_connector/redact.py +51 -0
- dq_export_connector/runner.py +87 -0
- dq_export_connector-0.1.0.dev1.dist-info/METADATA +47 -0
- dq_export_connector-0.1.0.dev1.dist-info/RECORD +19 -0
- dq_export_connector-0.1.0.dev1.dist-info/WHEEL +4 -0
|
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]
|