steve-cli 0.5.4__tar.gz → 0.5.6__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.
- {steve_cli-0.5.4 → steve_cli-0.5.6}/PKG-INFO +1 -1
- {steve_cli-0.5.4 → steve_cli-0.5.6}/pyproject.toml +1 -1
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/decorators/lineage_job.py +5 -1
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/s3.py +28 -14
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/trino.py +91 -10
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/PKG-INFO +1 -1
- {steve_cli-0.5.4 → steve_cli-0.5.6}/README.md +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/setup.cfg +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/__init__.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/auth.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/cli.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/decorators/__init__.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/__init__.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/__init__.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/composite.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/dataproductregistry.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/logging.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/null.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/openlineage.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/collector.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/port.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/registry.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/storage.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/ontology.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/policies/__init__.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/policies/client.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/__init__.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/__init__.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/__init__.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/csv.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/excel.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/generic.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/json.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/parquet.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/port.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/registry.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/parquet.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/protocol.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/__init__.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/adapters/__init__.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/adapters/great_expectations.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/adapters/null.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/adapters/validoopsie.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/port.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/registry.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/vkg.py +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/SOURCES.txt +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/dependency_links.txt +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/entry_points.txt +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/requires.txt +0 -0
- {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/top_level.txt +0 -0
|
@@ -67,8 +67,12 @@ def lineage_job(
|
|
|
67
67
|
import click
|
|
68
68
|
from steve_cli.validation.port import DataQualityError
|
|
69
69
|
if isinstance(exc, (DataQualityError, EnvironmentError)):
|
|
70
|
-
click.secho(f"ERROR {exc}", fg="red", bold=True, err=True)
|
|
70
|
+
click.secho(f"ERROR [{job_name}] {exc}", fg="red", bold=True, err=True)
|
|
71
71
|
sys.exit(1)
|
|
72
|
+
click.secho(
|
|
73
|
+
f"ERROR [{job_name}] {type(exc).__name__}: {exc}",
|
|
74
|
+
fg="red", bold=True, err=True,
|
|
75
|
+
)
|
|
72
76
|
raise
|
|
73
77
|
|
|
74
78
|
session.complete()
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
from pathlib import Path, PurePosixPath
|
|
6
|
-
from typing import List
|
|
7
6
|
|
|
8
7
|
import boto3
|
|
9
8
|
from botocore.client import Config
|
|
@@ -21,7 +20,7 @@ class S3Storage:
|
|
|
21
20
|
|
|
22
21
|
missing = [v for v in required_vars if not os.getenv(v)]
|
|
23
22
|
if missing:
|
|
24
|
-
raise
|
|
23
|
+
raise OSError(
|
|
25
24
|
f"Missing required environment variables for {self.tier} tier: "
|
|
26
25
|
f"{', '.join(missing)}"
|
|
27
26
|
)
|
|
@@ -78,22 +77,37 @@ class S3Storage:
|
|
|
78
77
|
|
|
79
78
|
def _handle_client_error(self, error: ClientError, path: str, operation: str) -> None:
|
|
80
79
|
code = error.response["Error"]["Code"]
|
|
80
|
+
key = self._key(path)
|
|
81
|
+
tier_upper = self.tier.upper()
|
|
82
|
+
location = f"s3://{self.bucket}/{key} at {self.endpoint}"
|
|
81
83
|
if code == "AccessDenied":
|
|
82
|
-
logger.error("Access
|
|
84
|
+
logger.error("S3Storage: Access Denied when %s [%s]", operation, location)
|
|
85
|
+
raise OSError(
|
|
86
|
+
f"S3Storage: {operation} failed — Access Denied [{location}] "
|
|
87
|
+
f"(check {tier_upper}_ACCESS_KEY / {tier_upper}_SECRET_KEY have permission)"
|
|
88
|
+
)
|
|
83
89
|
elif code == "NoSuchBucket":
|
|
84
|
-
logger.error("
|
|
90
|
+
logger.error("S3Storage: bucket not found [bucket=%s, endpoint=%s]", self.bucket, self.endpoint)
|
|
91
|
+
raise OSError(
|
|
92
|
+
f"S3Storage: {operation} failed — bucket '{self.bucket}' not found at {self.endpoint}"
|
|
93
|
+
)
|
|
85
94
|
elif code == "NoSuchKey":
|
|
86
|
-
logger.warning("
|
|
95
|
+
logger.warning("S3Storage: key not found when %s [%s]", operation, location)
|
|
96
|
+
raise OSError(
|
|
97
|
+
f"S3Storage: {operation} failed — key not found [s3://{self.bucket}/{key}]"
|
|
98
|
+
)
|
|
87
99
|
else:
|
|
88
|
-
logger.error("
|
|
89
|
-
|
|
100
|
+
logger.error("S3Storage: %s error when %s [%s]: %s", code, operation, location, error)
|
|
101
|
+
raise OSError(
|
|
102
|
+
f"S3Storage: {operation} failed — {code} [s3://{self.bucket}/{key} at {self.endpoint}]"
|
|
103
|
+
)
|
|
90
104
|
|
|
91
105
|
def put_file(self, local_path: str, path: str) -> None:
|
|
92
106
|
try:
|
|
93
107
|
self.client.upload_file(local_path, self.bucket, self._key(path))
|
|
94
108
|
logger.info("Uploaded %s", self._object_location(path))
|
|
95
109
|
except EndpointConnectionError:
|
|
96
|
-
raise
|
|
110
|
+
raise OSError(f"S3Storage: cannot connect to S3 endpoint at {self.endpoint} (uploading {path})")
|
|
97
111
|
except ClientError as e:
|
|
98
112
|
self._handle_client_error(e, path, "uploading")
|
|
99
113
|
|
|
@@ -103,7 +117,7 @@ class S3Storage:
|
|
|
103
117
|
self.client.download_file(self.bucket, self._key(path), local_path)
|
|
104
118
|
logger.info("Downloaded %s", self._object_location(path))
|
|
105
119
|
except EndpointConnectionError:
|
|
106
|
-
raise
|
|
120
|
+
raise OSError(f"S3Storage: cannot connect to S3 endpoint at {self.endpoint} (downloading {path})")
|
|
107
121
|
except ClientError as e:
|
|
108
122
|
self._handle_client_error(e, path, "downloading")
|
|
109
123
|
|
|
@@ -112,7 +126,7 @@ class S3Storage:
|
|
|
112
126
|
self.client.put_object(Bucket=self.bucket, Key=self._key(path), Body=data)
|
|
113
127
|
logger.info("Stored %s", self._object_location(path))
|
|
114
128
|
except EndpointConnectionError:
|
|
115
|
-
raise
|
|
129
|
+
raise OSError(f"S3Storage: cannot connect to S3 endpoint at {self.endpoint} (writing {path})")
|
|
116
130
|
except ClientError as e:
|
|
117
131
|
self._handle_client_error(e, path, "writing")
|
|
118
132
|
|
|
@@ -122,16 +136,16 @@ class S3Storage:
|
|
|
122
136
|
logger.info("Retrieved %s", self._object_location(path))
|
|
123
137
|
return obj["Body"].read()
|
|
124
138
|
except EndpointConnectionError:
|
|
125
|
-
raise
|
|
139
|
+
raise OSError(f"S3Storage: cannot connect to S3 endpoint at {self.endpoint} (reading {path})")
|
|
126
140
|
except ClientError as e:
|
|
127
141
|
self._handle_client_error(e, path, "reading")
|
|
128
142
|
|
|
129
|
-
def list(self, prefix: str = "") ->
|
|
130
|
-
keys:
|
|
143
|
+
def list(self, prefix: str = "") -> list[str]:
|
|
144
|
+
keys: list[str] = []
|
|
131
145
|
paginator = self.client.get_paginator("list_objects_v2")
|
|
132
146
|
for page in paginator.paginate(Bucket=self.bucket, Prefix=self._key(prefix)):
|
|
133
147
|
keys.extend(obj["Key"] for obj in page.get("Contents", []))
|
|
134
148
|
return keys
|
|
135
149
|
|
|
136
|
-
def list_all(self) ->
|
|
150
|
+
def list_all(self) -> list[str]:
|
|
137
151
|
return self.list()
|
|
@@ -26,12 +26,21 @@ class TrinoStorage:
|
|
|
26
26
|
self._workspace_id = os.getenv("WORKSPACE_ID")
|
|
27
27
|
trino_endpoint = get_service_url("trino", self._workspace_id)
|
|
28
28
|
if not trino_endpoint:
|
|
29
|
-
|
|
29
|
+
from steve_cli.auth import _get_workspace_entry
|
|
30
|
+
entry = _get_workspace_entry(self._workspace_id)
|
|
31
|
+
checks = [
|
|
32
|
+
("✓" if entry else "✗", "credentials found in ~/.steve/credentials.json"),
|
|
33
|
+
("✓" if entry.get("token") else "✗", "token present"),
|
|
34
|
+
("✓" if entry.get("base_url") else "✗", "base_url present (STEVE_BASE_URL configured on automation-kernel)"),
|
|
35
|
+
("✓" if self._workspace_id else "✗", f"WORKSPACE_ID env var set (got: {self._workspace_id!r})"),
|
|
36
|
+
]
|
|
37
|
+
detail = "\n".join(f" {mark} {desc}" for mark, desc in checks)
|
|
38
|
+
raise OSError(f"Trino endpoint not found:\n{detail}")
|
|
30
39
|
self.catalog = os.getenv("TRINO_CATALOG", "minio")
|
|
31
40
|
resolved_workspace = workspace or os.getenv("WORKSPACE_NAME")
|
|
32
41
|
self.schema = _derive_schema(tier, resolved_workspace) or os.environ.get("TRINO_SCHEMA", "")
|
|
33
42
|
if not self.schema:
|
|
34
|
-
raise
|
|
43
|
+
raise OSError("TRINO_SCHEMA is not set and no WORKSPACE_NAME or workspace was provided")
|
|
35
44
|
self.user = os.getenv("TRINO_USER", "admin")
|
|
36
45
|
self._base = trino_endpoint.rstrip("/")
|
|
37
46
|
self._lakekeeper_endpoint = os.getenv("LAKEKEEPER_ENDPOINT")
|
|
@@ -106,8 +115,19 @@ class TrinoStorage:
|
|
|
106
115
|
headers["Authorization"] = f"Bearer {token}"
|
|
107
116
|
|
|
108
117
|
t0 = time.perf_counter()
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
try:
|
|
119
|
+
resp = requests.post(f"{self._base}/v1/statement", data=sql, headers=headers)
|
|
120
|
+
except requests.exceptions.ConnectionError as exc:
|
|
121
|
+
raise OSError(
|
|
122
|
+
f"TrinoStorage: cannot connect to Trino at {self._base} — "
|
|
123
|
+
f"check WORKSPACE_ID env var and credentials in ~/.steve/credentials.json"
|
|
124
|
+
) from exc
|
|
125
|
+
if not resp.ok:
|
|
126
|
+
raise OSError(
|
|
127
|
+
f"TrinoStorage: POST /v1/statement failed with HTTP {resp.status_code} "
|
|
128
|
+
f"[url={self._base}] — auth proxy may not be routing to Trino, "
|
|
129
|
+
f"or the workspace is not accessible (sql: {sql[:100]!r})"
|
|
130
|
+
)
|
|
111
131
|
if trace:
|
|
112
132
|
logger.warning("[trino] POST /v1/statement: %.3fs", time.perf_counter() - t0)
|
|
113
133
|
|
|
@@ -125,8 +145,18 @@ class TrinoStorage:
|
|
|
125
145
|
time.sleep(0.1)
|
|
126
146
|
next_uri = next_uri.replace("http://trino:8080", self._base).replace("https://trino:8080", self._base)
|
|
127
147
|
t_poll = time.perf_counter()
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
try:
|
|
149
|
+
resp = requests.get(next_uri, headers=headers)
|
|
150
|
+
except requests.exceptions.ConnectionError as exc:
|
|
151
|
+
raise OSError(
|
|
152
|
+
f"TrinoStorage: connection lost while polling Trino result [url={next_uri}] — "
|
|
153
|
+
f"sql: {sql[:100]!r}"
|
|
154
|
+
) from exc
|
|
155
|
+
if not resp.ok:
|
|
156
|
+
raise OSError(
|
|
157
|
+
f"TrinoStorage: result poll failed with HTTP {resp.status_code} "
|
|
158
|
+
f"[url={next_uri}] — Trino rejected query (sql: {sql[:100]!r})"
|
|
159
|
+
)
|
|
130
160
|
poll += 1
|
|
131
161
|
if trace:
|
|
132
162
|
logger.warning("[trino] poll #%d: %.3fs (total %.3fs, %d rows so far)",
|
|
@@ -153,7 +183,15 @@ class TrinoStorage:
|
|
|
153
183
|
return self.list()
|
|
154
184
|
|
|
155
185
|
def get_bytes(self, path: str) -> bytes:
|
|
156
|
-
|
|
186
|
+
table_ref = f"{self.catalog}.{self.schema}.{self._table_name(path)}"
|
|
187
|
+
try:
|
|
188
|
+
rows = self._execute(f"SELECT * FROM {table_ref}")
|
|
189
|
+
except OSError:
|
|
190
|
+
raise
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
raise OSError(
|
|
193
|
+
f"TrinoStorage: read from '{table_ref}' failed — {type(exc).__name__}: {exc}"
|
|
194
|
+
) from exc
|
|
157
195
|
if not rows:
|
|
158
196
|
return b""
|
|
159
197
|
arrow_table = pa.Table.from_pylist(rows)
|
|
@@ -167,11 +205,24 @@ class TrinoStorage:
|
|
|
167
205
|
|
|
168
206
|
def _ensure_namespace(self) -> None:
|
|
169
207
|
from pyiceberg.exceptions import NamespaceAlreadyExistsError
|
|
208
|
+
lakekeeper_base = self._lakekeeper_endpoint or "http://lakekeeper:8181/catalog"
|
|
170
209
|
try:
|
|
171
210
|
self._iceberg_catalog.create_namespace(self.schema)
|
|
172
211
|
logger.info("Created namespace %s", self.schema)
|
|
173
212
|
except NamespaceAlreadyExistsError:
|
|
174
213
|
pass
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
msg = str(exc)
|
|
216
|
+
if "403" in msg or "Access Denied" in msg or "Forbidden" in msg:
|
|
217
|
+
raise OSError(
|
|
218
|
+
f"TrinoStorage: namespace '{self.schema}' creation denied at Lakekeeper "
|
|
219
|
+
f"[{lakekeeper_base}] — check that the token has write access to warehouse "
|
|
220
|
+
f"'{self._lakekeeper_warehouse}'"
|
|
221
|
+
) from exc
|
|
222
|
+
raise OSError(
|
|
223
|
+
f"TrinoStorage: namespace '{self.schema}' creation failed at Lakekeeper "
|
|
224
|
+
f"[{lakekeeper_base}] — {type(exc).__name__}: {exc}"
|
|
225
|
+
) from exc
|
|
175
226
|
|
|
176
227
|
def put_bytes(self, data: bytes, path: str) -> None:
|
|
177
228
|
from pyiceberg.exceptions import NoSuchTableError
|
|
@@ -179,12 +230,29 @@ class TrinoStorage:
|
|
|
179
230
|
arrow_table = pq.read_table(io.BytesIO(data))
|
|
180
231
|
table_id = f"{self.schema}.{self._table_name(path)}"
|
|
181
232
|
catalog = self._iceberg_catalog
|
|
233
|
+
lakekeeper_base = self._lakekeeper_endpoint or "http://lakekeeper:8181/catalog"
|
|
182
234
|
|
|
183
235
|
self._ensure_namespace()
|
|
184
236
|
|
|
237
|
+
def _append(iceberg_table, arrow_table, table_id):
|
|
238
|
+
try:
|
|
239
|
+
iceberg_table.append(arrow_table)
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
msg = str(exc)
|
|
242
|
+
if "403" in msg or "Access Denied" in msg or "Forbidden" in msg:
|
|
243
|
+
raise OSError(
|
|
244
|
+
f"TrinoStorage: write to '{table_id}' denied via Lakekeeper [{lakekeeper_base}] — "
|
|
245
|
+
f"Access Denied on warehouse '{self._lakekeeper_warehouse}' "
|
|
246
|
+
f"(check S3 bucket policy and token permissions)"
|
|
247
|
+
) from exc
|
|
248
|
+
raise OSError(
|
|
249
|
+
f"TrinoStorage: write to '{table_id}' failed via Lakekeeper [{lakekeeper_base}] — "
|
|
250
|
+
f"{type(exc).__name__}: {exc}"
|
|
251
|
+
) from exc
|
|
252
|
+
|
|
185
253
|
try:
|
|
186
254
|
iceberg_table = catalog.load_table(table_id)
|
|
187
|
-
iceberg_table
|
|
255
|
+
_append(iceberg_table, arrow_table, table_id)
|
|
188
256
|
except NoSuchTableError:
|
|
189
257
|
from pyiceberg.schema import Schema
|
|
190
258
|
from pyiceberg.types import (
|
|
@@ -221,8 +289,21 @@ class TrinoStorage:
|
|
|
221
289
|
fields.append(NestedField(field_id=i + 1, name=field.name, field_type=iceberg_type, required=not field.nullable))
|
|
222
290
|
|
|
223
291
|
iceberg_schema = Schema(*fields)
|
|
224
|
-
|
|
225
|
-
|
|
292
|
+
try:
|
|
293
|
+
iceberg_table = catalog.create_table(table_id, schema=iceberg_schema)
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
msg = str(exc)
|
|
296
|
+
if "403" in msg or "Access Denied" in msg or "Forbidden" in msg:
|
|
297
|
+
raise OSError(
|
|
298
|
+
f"TrinoStorage: create table '{table_id}' denied via Lakekeeper [{lakekeeper_base}] — "
|
|
299
|
+
f"Access Denied on warehouse '{self._lakekeeper_warehouse}' "
|
|
300
|
+
f"(check S3 bucket policy and token permissions)"
|
|
301
|
+
) from exc
|
|
302
|
+
raise OSError(
|
|
303
|
+
f"TrinoStorage: create table '{table_id}' failed via Lakekeeper [{lakekeeper_base}] — "
|
|
304
|
+
f"{type(exc).__name__}: {exc}"
|
|
305
|
+
) from exc
|
|
306
|
+
_append(iceberg_table, arrow_table, table_id)
|
|
226
307
|
|
|
227
308
|
logger.info("Written %d rows to %s", len(arrow_table), table_id)
|
|
228
309
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|