avrokit 0.1.3__tar.gz → 0.1.4__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.
- {avrokit-0.1.3 → avrokit-0.1.4}/PKG-INFO +28 -30
- {avrokit-0.1.3 → avrokit-0.1.4}/README.md +27 -29
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/asyncio/writer.py +14 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/io/reader.py +2 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/io/writer.py +2 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/concat.py +4 -2
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/filesort.py +1 -1
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/repair.py +1 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/url/http.py +12 -1
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/url/s3.py +11 -1
- {avrokit-0.1.3 → avrokit-0.1.4}/pyproject.toml +1 -1
- avrokit-0.1.4/uv.lock +1522 -0
- avrokit-0.1.3/uv.lock +0 -1533
- {avrokit-0.1.3 → avrokit-0.1.4}/.github/workflows/check.yml +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/.github/workflows/release.yml +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/.gitignore +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/LICENSE +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/LICENSES/Apache-2.0.txt +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/Makefile +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/__init__.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/asyncio/__init__.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/asyncio/reader.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/io/__init__.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/io/compact.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/io/schema.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/py.typed +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/__init__.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/__main__.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/base.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/cat.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/count.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/fromparquet.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/getmeta.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/getschema.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/httpserver.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/partition.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/stats.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/tojson.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/tools/toparquet.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/url/__init__.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/url/base.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/url/factory.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/url/file.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/url/google.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/avrokit/url/utils.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/__init__.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_additional_coverage.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_asyncio.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_exports.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_io.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_io_schema.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_plan_coverage.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_property_based.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_schema_advanced.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_tools_httpserver.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_tools_integration.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_tools_repair.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_url.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_url_contract.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_url_file.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_url_google.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_url_http.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/tests/test_url_s3.py +0 -0
- {avrokit-0.1.3 → avrokit-0.1.4}/uv.lock.license +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: avrokit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Python utilities for working with Avro data files
|
|
5
5
|
Project-URL: Homepage, https://github.com/brandtg/avrokit
|
|
6
6
|
Project-URL: Repository, https://github.com/brandtg/avrokit
|
|
@@ -116,24 +116,23 @@ avrokit concat file1.avro file2.avro s3://bucket/file3.avro output.avro
|
|
|
116
116
|
from avrokit import avro_reader, avro_records, avro_writer, avro_schema, parse_url
|
|
117
117
|
|
|
118
118
|
# Read records from any URL
|
|
119
|
-
url = parse_url(
|
|
119
|
+
url = parse_url("gs://bucket/data.avro", mode="rb")
|
|
120
120
|
for record in avro_records(url):
|
|
121
121
|
print(record)
|
|
122
122
|
|
|
123
123
|
# Write Avro data
|
|
124
|
-
schema = avro_schema(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
url = parse_url('output.avro', mode='wb')
|
|
124
|
+
schema = avro_schema(
|
|
125
|
+
{
|
|
126
|
+
"type": "record",
|
|
127
|
+
"name": "User",
|
|
128
|
+
"fields": [{"name": "name", "type": "string"}, {"name": "age", "type": "int"}],
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
url = parse_url("output.avro", mode="wb")
|
|
134
133
|
with avro_writer(url, schema) as writer:
|
|
135
|
-
writer.append({
|
|
136
|
-
writer.append({
|
|
134
|
+
writer.append({"name": "Alice", "age": 30})
|
|
135
|
+
writer.append({"name": "Bob", "age": 25})
|
|
137
136
|
```
|
|
138
137
|
|
|
139
138
|
## Architecture
|
|
@@ -156,10 +155,10 @@ The `parse_url()` factory function automatically instantiates the correct URL cl
|
|
|
156
155
|
from avrokit.url import parse_url
|
|
157
156
|
|
|
158
157
|
# All of these return the appropriate URL subclass
|
|
159
|
-
url1 = parse_url(
|
|
160
|
-
url2 = parse_url(
|
|
161
|
-
url3 = parse_url(
|
|
162
|
-
url4 = parse_url(
|
|
158
|
+
url1 = parse_url("file:///path/to/data.avro") # FileURL
|
|
159
|
+
url2 = parse_url("gs://bucket/data.avro") # GCSURL
|
|
160
|
+
url3 = parse_url("s3://bucket/data.avro") # S3URL
|
|
161
|
+
url4 = parse_url("https://example.com/data.avro") # HTTPURL
|
|
163
162
|
```
|
|
164
163
|
|
|
165
164
|
### 2. I/O Layer (`avrokit.io`)
|
|
@@ -401,13 +400,13 @@ The GitHub Actions workflow will automatically build and publish to PyPI.
|
|
|
401
400
|
from avrokit import PartitionedAvroReader, PartitionedAvroWriter, parse_url
|
|
402
401
|
|
|
403
402
|
# Read from multiple files as single stream
|
|
404
|
-
url = parse_url(
|
|
403
|
+
url = parse_url("data-*.avro", mode="rb")
|
|
405
404
|
with PartitionedAvroReader(url) as reader:
|
|
406
405
|
for record in reader:
|
|
407
406
|
process(record)
|
|
408
407
|
|
|
409
408
|
# Write to multiple files (roll every 10000 records)
|
|
410
|
-
url = parse_url(
|
|
409
|
+
url = parse_url("output/", mode="wb")
|
|
411
410
|
with PartitionedAvroWriter(url, schema, max_records=10000) as writer:
|
|
412
411
|
for record in records:
|
|
413
412
|
writer.append(record)
|
|
@@ -422,15 +421,12 @@ from avrokit import TimePartitionedAvroWriter, parse_url
|
|
|
422
421
|
from datetime import datetime
|
|
423
422
|
|
|
424
423
|
# Creates hourly files: output/2024/01/15/10.avro
|
|
425
|
-
url = parse_url(
|
|
424
|
+
url = parse_url("output/", mode="wb")
|
|
426
425
|
with TimePartitionedAvroWriter(
|
|
427
|
-
url,
|
|
428
|
-
schema,
|
|
429
|
-
time_granularity='hour',
|
|
430
|
-
time_field='timestamp'
|
|
426
|
+
url, schema, time_granularity="hour", time_field="timestamp"
|
|
431
427
|
) as writer:
|
|
432
428
|
for record in records:
|
|
433
|
-
writer.append(record, timestamp=record[
|
|
429
|
+
writer.append(record, timestamp=record["timestamp"])
|
|
434
430
|
```
|
|
435
431
|
|
|
436
432
|
### Async Operations
|
|
@@ -440,8 +436,9 @@ import asyncio
|
|
|
440
436
|
from avrokit.asyncio import DeferredAvroWriter
|
|
441
437
|
from avrokit import parse_url, avro_schema
|
|
442
438
|
|
|
439
|
+
|
|
443
440
|
async def write_async():
|
|
444
|
-
url = parse_url(
|
|
441
|
+
url = parse_url("output.avro", mode="wb")
|
|
445
442
|
schema = avro_schema({...})
|
|
446
443
|
|
|
447
444
|
async with DeferredAvroWriter(url, schema) as writer:
|
|
@@ -449,6 +446,7 @@ async def write_async():
|
|
|
449
446
|
await writer.append(record)
|
|
450
447
|
# Flushes happen automatically in background
|
|
451
448
|
|
|
449
|
+
|
|
452
450
|
asyncio.run(write_async())
|
|
453
451
|
```
|
|
454
452
|
|
|
@@ -459,13 +457,13 @@ from avrokit import validate_avro_schema_evolution, read_avro_schema
|
|
|
459
457
|
from avrokit.url import parse_url
|
|
460
458
|
|
|
461
459
|
# Check if new schema is backward compatible
|
|
462
|
-
reader_schema = read_avro_schema(parse_url(
|
|
463
|
-
writer_schema = read_avro_schema(parse_url(
|
|
460
|
+
reader_schema = read_avro_schema(parse_url("old.avro", "rb"))
|
|
461
|
+
writer_schema = read_avro_schema(parse_url("new.avro", "rb"))
|
|
464
462
|
|
|
465
463
|
is_valid = validate_avro_schema_evolution(
|
|
466
464
|
reader_schema,
|
|
467
465
|
writer_schema,
|
|
468
|
-
strategy=
|
|
466
|
+
strategy="backward", # or 'forward', 'full'
|
|
469
467
|
)
|
|
470
468
|
```
|
|
471
469
|
|
|
@@ -77,24 +77,23 @@ avrokit concat file1.avro file2.avro s3://bucket/file3.avro output.avro
|
|
|
77
77
|
from avrokit import avro_reader, avro_records, avro_writer, avro_schema, parse_url
|
|
78
78
|
|
|
79
79
|
# Read records from any URL
|
|
80
|
-
url = parse_url(
|
|
80
|
+
url = parse_url("gs://bucket/data.avro", mode="rb")
|
|
81
81
|
for record in avro_records(url):
|
|
82
82
|
print(record)
|
|
83
83
|
|
|
84
84
|
# Write Avro data
|
|
85
|
-
schema = avro_schema(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
url = parse_url('output.avro', mode='wb')
|
|
85
|
+
schema = avro_schema(
|
|
86
|
+
{
|
|
87
|
+
"type": "record",
|
|
88
|
+
"name": "User",
|
|
89
|
+
"fields": [{"name": "name", "type": "string"}, {"name": "age", "type": "int"}],
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
url = parse_url("output.avro", mode="wb")
|
|
95
94
|
with avro_writer(url, schema) as writer:
|
|
96
|
-
writer.append({
|
|
97
|
-
writer.append({
|
|
95
|
+
writer.append({"name": "Alice", "age": 30})
|
|
96
|
+
writer.append({"name": "Bob", "age": 25})
|
|
98
97
|
```
|
|
99
98
|
|
|
100
99
|
## Architecture
|
|
@@ -117,10 +116,10 @@ The `parse_url()` factory function automatically instantiates the correct URL cl
|
|
|
117
116
|
from avrokit.url import parse_url
|
|
118
117
|
|
|
119
118
|
# All of these return the appropriate URL subclass
|
|
120
|
-
url1 = parse_url(
|
|
121
|
-
url2 = parse_url(
|
|
122
|
-
url3 = parse_url(
|
|
123
|
-
url4 = parse_url(
|
|
119
|
+
url1 = parse_url("file:///path/to/data.avro") # FileURL
|
|
120
|
+
url2 = parse_url("gs://bucket/data.avro") # GCSURL
|
|
121
|
+
url3 = parse_url("s3://bucket/data.avro") # S3URL
|
|
122
|
+
url4 = parse_url("https://example.com/data.avro") # HTTPURL
|
|
124
123
|
```
|
|
125
124
|
|
|
126
125
|
### 2. I/O Layer (`avrokit.io`)
|
|
@@ -362,13 +361,13 @@ The GitHub Actions workflow will automatically build and publish to PyPI.
|
|
|
362
361
|
from avrokit import PartitionedAvroReader, PartitionedAvroWriter, parse_url
|
|
363
362
|
|
|
364
363
|
# Read from multiple files as single stream
|
|
365
|
-
url = parse_url(
|
|
364
|
+
url = parse_url("data-*.avro", mode="rb")
|
|
366
365
|
with PartitionedAvroReader(url) as reader:
|
|
367
366
|
for record in reader:
|
|
368
367
|
process(record)
|
|
369
368
|
|
|
370
369
|
# Write to multiple files (roll every 10000 records)
|
|
371
|
-
url = parse_url(
|
|
370
|
+
url = parse_url("output/", mode="wb")
|
|
372
371
|
with PartitionedAvroWriter(url, schema, max_records=10000) as writer:
|
|
373
372
|
for record in records:
|
|
374
373
|
writer.append(record)
|
|
@@ -383,15 +382,12 @@ from avrokit import TimePartitionedAvroWriter, parse_url
|
|
|
383
382
|
from datetime import datetime
|
|
384
383
|
|
|
385
384
|
# Creates hourly files: output/2024/01/15/10.avro
|
|
386
|
-
url = parse_url(
|
|
385
|
+
url = parse_url("output/", mode="wb")
|
|
387
386
|
with TimePartitionedAvroWriter(
|
|
388
|
-
url,
|
|
389
|
-
schema,
|
|
390
|
-
time_granularity='hour',
|
|
391
|
-
time_field='timestamp'
|
|
387
|
+
url, schema, time_granularity="hour", time_field="timestamp"
|
|
392
388
|
) as writer:
|
|
393
389
|
for record in records:
|
|
394
|
-
writer.append(record, timestamp=record[
|
|
390
|
+
writer.append(record, timestamp=record["timestamp"])
|
|
395
391
|
```
|
|
396
392
|
|
|
397
393
|
### Async Operations
|
|
@@ -401,8 +397,9 @@ import asyncio
|
|
|
401
397
|
from avrokit.asyncio import DeferredAvroWriter
|
|
402
398
|
from avrokit import parse_url, avro_schema
|
|
403
399
|
|
|
400
|
+
|
|
404
401
|
async def write_async():
|
|
405
|
-
url = parse_url(
|
|
402
|
+
url = parse_url("output.avro", mode="wb")
|
|
406
403
|
schema = avro_schema({...})
|
|
407
404
|
|
|
408
405
|
async with DeferredAvroWriter(url, schema) as writer:
|
|
@@ -410,6 +407,7 @@ async def write_async():
|
|
|
410
407
|
await writer.append(record)
|
|
411
408
|
# Flushes happen automatically in background
|
|
412
409
|
|
|
410
|
+
|
|
413
411
|
asyncio.run(write_async())
|
|
414
412
|
```
|
|
415
413
|
|
|
@@ -420,13 +418,13 @@ from avrokit import validate_avro_schema_evolution, read_avro_schema
|
|
|
420
418
|
from avrokit.url import parse_url
|
|
421
419
|
|
|
422
420
|
# Check if new schema is backward compatible
|
|
423
|
-
reader_schema = read_avro_schema(parse_url(
|
|
424
|
-
writer_schema = read_avro_schema(parse_url(
|
|
421
|
+
reader_schema = read_avro_schema(parse_url("old.avro", "rb"))
|
|
422
|
+
writer_schema = read_avro_schema(parse_url("new.avro", "rb"))
|
|
425
423
|
|
|
426
424
|
is_valid = validate_avro_schema_evolution(
|
|
427
425
|
reader_schema,
|
|
428
426
|
writer_schema,
|
|
429
|
-
strategy=
|
|
427
|
+
strategy="backward", # or 'forward', 'full'
|
|
430
428
|
)
|
|
431
429
|
```
|
|
432
430
|
|
|
@@ -43,6 +43,20 @@ class DeferredAvroWriter:
|
|
|
43
43
|
self._writer_thread_done.set()
|
|
44
44
|
self._writer_thread.join()
|
|
45
45
|
|
|
46
|
+
def close(self) -> None:
|
|
47
|
+
self.stop()
|
|
48
|
+
# Close the underlying writer if it supports it (e.g. DataFileWriter)
|
|
49
|
+
close_fn = getattr(self.writer, "close", None)
|
|
50
|
+
if close_fn:
|
|
51
|
+
close_fn()
|
|
52
|
+
|
|
53
|
+
def __enter__(self) -> "DeferredAvroWriter":
|
|
54
|
+
self.start()
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
58
|
+
self.close()
|
|
59
|
+
|
|
46
60
|
def append(
|
|
47
61
|
self, datum: object, block: bool = True, timeout: int | None = None
|
|
48
62
|
) -> None:
|
|
@@ -80,11 +80,13 @@ class ConcatTool:
|
|
|
80
80
|
|
|
81
81
|
output_sync_marker = None
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
out_url = output_url.with_mode("wb")
|
|
84
|
+
with out_url as out_f:
|
|
84
85
|
writer = BinaryEncoder(out_f)
|
|
85
86
|
|
|
86
87
|
for i, url in enumerate(input_urls):
|
|
87
|
-
|
|
88
|
+
in_url = url.with_mode("rb")
|
|
89
|
+
with in_url as in_f:
|
|
88
90
|
# Initialize DataFileReader to parse the header/metadata and
|
|
89
91
|
# position the file pointer at the start of the first block.
|
|
90
92
|
reader = DataFileReader(in_f, DatumReader())
|
|
@@ -147,6 +147,7 @@ class RepairTool:
|
|
|
147
147
|
if dry_run:
|
|
148
148
|
tmp = tempfile.NamedTemporaryFile(delete=False)
|
|
149
149
|
output_url = parse_url(tmp.name, mode="wb")
|
|
150
|
+
tmp.close() # Close the handle; the URL will open it fresh
|
|
150
151
|
logger.debug("Using temporary file %s for dry run", output_url)
|
|
151
152
|
# Initialize the output file with the same schema and codec
|
|
152
153
|
with avro_writer(output_url, schema, codec=codec) as writer:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
5
|
import io
|
|
6
|
+
import os
|
|
6
7
|
import tempfile
|
|
7
8
|
from collections.abc import Sequence
|
|
8
9
|
from typing import IO, Any
|
|
@@ -34,6 +35,7 @@ class HttpURL(URL):
|
|
|
34
35
|
self.content_type = content_type
|
|
35
36
|
self._current_response: requests.Response | None = None
|
|
36
37
|
self._current_request_buffer: IO[Any] | None = None
|
|
38
|
+
self._current_request_temp_path: str | None = None
|
|
37
39
|
|
|
38
40
|
@override
|
|
39
41
|
def _expand(self) -> Sequence[URL]:
|
|
@@ -84,9 +86,11 @@ class HttpURL(URL):
|
|
|
84
86
|
if self.spill_request_to_file:
|
|
85
87
|
# Use a temporary file to store the request body
|
|
86
88
|
buf = tempfile.NamedTemporaryFile(delete=False)
|
|
89
|
+
self._current_request_temp_path = buf.name
|
|
87
90
|
else:
|
|
88
91
|
# Use an in-memory buffer
|
|
89
92
|
buf = io.BytesIO()
|
|
93
|
+
self._current_request_temp_path = None
|
|
90
94
|
# Wrap in TextIOWrapper if not binary mode
|
|
91
95
|
if "b" not in self.mode:
|
|
92
96
|
buf = io.TextIOWrapper(buf, encoding="utf-8")
|
|
@@ -112,8 +116,15 @@ class HttpURL(URL):
|
|
|
112
116
|
headers={"Content-Type": self.content_type},
|
|
113
117
|
)
|
|
114
118
|
res.raise_for_status()
|
|
115
|
-
# Close the buffer
|
|
119
|
+
# Close the buffer
|
|
116
120
|
self._current_request_buffer.close()
|
|
121
|
+
# Unlink temp file if used
|
|
122
|
+
if self._current_request_temp_path is not None:
|
|
123
|
+
try:
|
|
124
|
+
os.unlink(self._current_request_temp_path)
|
|
125
|
+
except FileNotFoundError:
|
|
126
|
+
pass
|
|
127
|
+
self._current_request_temp_path = None
|
|
117
128
|
|
|
118
129
|
@override
|
|
119
130
|
def with_mode(self, mode) -> URL:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
5
|
import io
|
|
6
|
+
import os
|
|
6
7
|
import tempfile
|
|
7
8
|
from collections.abc import Sequence
|
|
8
9
|
from contextlib import contextmanager
|
|
@@ -125,7 +126,16 @@ class S3URL(URL):
|
|
|
125
126
|
self._current_client.upload_fileobj(f, self.bucket, self.path)
|
|
126
127
|
finally:
|
|
127
128
|
# Cleanup resources
|
|
128
|
-
self._current_local
|
|
129
|
+
local = self._current_local
|
|
130
|
+
name = local.name
|
|
131
|
+
local.close()
|
|
132
|
+
# N.b. the temp file is created with delete=False so it survives an external
|
|
133
|
+
# close (e.g. DataFileWriter closing the stream before URL.close() uploads).
|
|
134
|
+
# Unlink it explicitly here to avoid leaking a file per open/close cycle.
|
|
135
|
+
try:
|
|
136
|
+
os.unlink(name)
|
|
137
|
+
except FileNotFoundError:
|
|
138
|
+
pass
|
|
129
139
|
self._current_client.close()
|
|
130
140
|
self._current_local = None
|
|
131
141
|
self._current_client = None
|