log-foundry 0.6.1.dev2__tar.gz → 0.6.1.dev3__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.
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/PKG-INFO +11 -2
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/README.md +10 -1
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/pyproject.toml +1 -1
- log_foundry-0.6.1.dev3/src/log_foundry/sinks/_batch.py +70 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/firehose.py +23 -9
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/kinesis.py +22 -12
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/LICENSE +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/api.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/config.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/console.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/context.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/model.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/sinks/util.py +0 -0
- {log_foundry-0.6.1.dev2 → log_foundry-0.6.1.dev3}/src/log_foundry/worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: log-foundry
|
|
3
|
-
Version: 0.6.1.
|
|
3
|
+
Version: 0.6.1.dev3
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -394,7 +394,8 @@ A few conventions hold across every sink below:
|
|
|
394
394
|
- **Ownership.** A resource the sink opens itself is closed on `shutdown()`; an injected one is left
|
|
395
395
|
open for you to manage.
|
|
396
396
|
- **Never crashes the app.** A failing sink is retried with backoff and then counted (`.failed`,
|
|
397
|
-
`.dropped_oversized`, …) rather than raised — a broken destination
|
|
397
|
+
`.dropped_oversized`, `.dropped_unadjudicated`, …) rather than raised — a broken destination
|
|
398
|
+
degrades logging, nothing more.
|
|
398
399
|
The one deliberate exception is a `MultiSink` whose children *all* failed: it re-raises so the
|
|
399
400
|
worker's retry engages, since nothing was delivered and there are no duplicates to risk. That
|
|
400
401
|
still doesn't reach your code — the worker is what catches it.
|
|
@@ -513,6 +514,14 @@ chain — log-foundry adds none of its own. Each re-chunks every batch to the se
|
|
|
513
514
|
limits, retries partial failures, and drops any single event too large to ever fit (counted on
|
|
514
515
|
`.dropped_oversized`).
|
|
515
516
|
|
|
517
|
+
`KinesisSink` and `FirehoseSink` learn which records failed **positionally** — the response carries a
|
|
518
|
+
parallel array with no ids — so they check that it describes as many records as were sent before
|
|
519
|
+
acting on it. A response that doesn't is not used to adjudicate any record in the chunk: the chunk is
|
|
520
|
+
abandoned rather than re-sent (some of it almost certainly landed), counted on
|
|
521
|
+
`.dropped_unadjudicated`, and named on stderr. A non-zero value there is real loss, and normally
|
|
522
|
+
means the client isn't AWS-shaped. `SQSSink` and `SNSSink` correlate by explicit `Id` instead, so
|
|
523
|
+
they can't mis-pair and have no such counter.
|
|
524
|
+
|
|
516
525
|
| Sink | Import from | Configure |
|
|
517
526
|
|---|---|---|
|
|
518
527
|
| `SQSSink` | `log_foundry.sinks.sqs` | `SQSSink(queue_url, *, max_retries=3, fifo=None, message_group_id=None, message_deduplication_id=None)` — the headline production path: a durable buffer in front of ELK, absorbing downstream spikes/outages. Standard **and** FIFO queues |
|
|
@@ -358,7 +358,8 @@ A few conventions hold across every sink below:
|
|
|
358
358
|
- **Ownership.** A resource the sink opens itself is closed on `shutdown()`; an injected one is left
|
|
359
359
|
open for you to manage.
|
|
360
360
|
- **Never crashes the app.** A failing sink is retried with backoff and then counted (`.failed`,
|
|
361
|
-
`.dropped_oversized`, …) rather than raised — a broken destination
|
|
361
|
+
`.dropped_oversized`, `.dropped_unadjudicated`, …) rather than raised — a broken destination
|
|
362
|
+
degrades logging, nothing more.
|
|
362
363
|
The one deliberate exception is a `MultiSink` whose children *all* failed: it re-raises so the
|
|
363
364
|
worker's retry engages, since nothing was delivered and there are no duplicates to risk. That
|
|
364
365
|
still doesn't reach your code — the worker is what catches it.
|
|
@@ -477,6 +478,14 @@ chain — log-foundry adds none of its own. Each re-chunks every batch to the se
|
|
|
477
478
|
limits, retries partial failures, and drops any single event too large to ever fit (counted on
|
|
478
479
|
`.dropped_oversized`).
|
|
479
480
|
|
|
481
|
+
`KinesisSink` and `FirehoseSink` learn which records failed **positionally** — the response carries a
|
|
482
|
+
parallel array with no ids — so they check that it describes as many records as were sent before
|
|
483
|
+
acting on it. A response that doesn't is not used to adjudicate any record in the chunk: the chunk is
|
|
484
|
+
abandoned rather than re-sent (some of it almost certainly landed), counted on
|
|
485
|
+
`.dropped_unadjudicated`, and named on stderr. A non-zero value there is real loss, and normally
|
|
486
|
+
means the client isn't AWS-shaped. `SQSSink` and `SNSSink` correlate by explicit `Id` instead, so
|
|
487
|
+
they can't mis-pair and have no such counter.
|
|
488
|
+
|
|
480
489
|
| Sink | Import from | Configure |
|
|
481
490
|
|---|---|---|
|
|
482
491
|
| `SQSSink` | `log_foundry.sinks.sqs` | `SQSSink(queue_url, *, max_retries=3, fifo=None, message_group_id=None, message_deduplication_id=None)` — the headline production path: a durable buffer in front of ELK, absorbing downstream spikes/outages. Standard **and** FIFO queues |
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Shared adjudication of a positional batch response (SPEC-018).
|
|
2
|
+
|
|
3
|
+
Some batch APIs report per-record outcomes **positionally**: the response carries a parallel array
|
|
4
|
+
with no identifiers, so entry *i* describes record *i*. That correlation holds only while the two
|
|
5
|
+
arrays are the same length, and a disagreement is evidence they are not aligned — not an
|
|
6
|
+
invitation to use the overlapping prefix. Pairing them anyway truncates silently, which reads
|
|
7
|
+
downstream as "everything landed" for records the destination never confirmed.
|
|
8
|
+
|
|
9
|
+
This module makes the precondition explicit: either the response describes the whole chunk and the
|
|
10
|
+
caller acts on it, or it does not and the caller abandons the chunk audibly. Id-keyed responses
|
|
11
|
+
(``SQSSink``, ``SNSSink``) select by id and cannot truncate, so they do not come through here.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, NamedTuple
|
|
17
|
+
|
|
18
|
+
__all__ = ["Adjudication", "adjudicate_positional", "usable_results"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Adjudication[T](NamedTuple):
|
|
22
|
+
"""The outcome of pairing a positional batch response against the records it should describe.
|
|
23
|
+
|
|
24
|
+
``retry`` is non-empty only when ``unadjudicated`` is ``0``: the response either describes the
|
|
25
|
+
chunk or it does not. The two are never both non-zero.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
retry: Records the response explicitly flagged as failed.
|
|
29
|
+
unadjudicated: Records whose outcome the response did not describe — ``0`` on a
|
|
30
|
+
well-formed response.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
retry: list[T]
|
|
34
|
+
unadjudicated: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def usable_results(results: Any) -> list[dict[str, Any]]:
|
|
38
|
+
"""Return ``results`` if it is a list of mappings, else an empty list.
|
|
39
|
+
|
|
40
|
+
The response comes off a client the sink does not control, so the field may be any shape at
|
|
41
|
+
all — ``None``, a scalar, a list of non-mappings. None of those carry per-record outcomes that
|
|
42
|
+
can be read, so each is treated as describing nothing, which routes it to the same counted,
|
|
43
|
+
audible abandonment as a length mismatch. Raising instead would put the malformed-client case
|
|
44
|
+
back on the path this module exists to take it off.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(results, list) and all(isinstance(result, dict) for result in results):
|
|
47
|
+
return results
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def adjudicate_positional[T](
|
|
52
|
+
records: list[T],
|
|
53
|
+
results: list[dict[str, Any]],
|
|
54
|
+
*,
|
|
55
|
+
error_key: str = "ErrorCode",
|
|
56
|
+
) -> Adjudication[T]:
|
|
57
|
+
"""Pair a positional batch response against the records it should describe.
|
|
58
|
+
|
|
59
|
+
Returns the subset of ``records`` whose paired ``results`` entry carries a truthy ``error_key``.
|
|
60
|
+
If the arrays disagree in length — including a ``results`` the caller defaulted to empty
|
|
61
|
+
because the response omitted it — nothing is selected and the chunk is reported unadjudicated.
|
|
62
|
+
``error_key`` is a parameter so a future positional response naming its error differently needs
|
|
63
|
+
no fork of this rule.
|
|
64
|
+
"""
|
|
65
|
+
if len(results) != len(records):
|
|
66
|
+
return Adjudication([], len(records))
|
|
67
|
+
return Adjudication(
|
|
68
|
+
[record for record, result in zip(records, results, strict=True) if result.get(error_key)],
|
|
69
|
+
0,
|
|
70
|
+
)
|
|
@@ -12,13 +12,23 @@ import json
|
|
|
12
12
|
import sys
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
+
from log_foundry.sinks._batch import adjudicate_positional, usable_results
|
|
15
16
|
from log_foundry.sinks._chunk import chunk_items
|
|
16
17
|
|
|
17
18
|
__all__ = ["FirehoseSink"]
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class FirehoseSink:
|
|
21
|
-
"""A :class:`~log_foundry.sinks.base.Sink` that writes events to a Firehose delivery stream.
|
|
22
|
+
"""A :class:`~log_foundry.sinks.base.Sink` that writes events to a Firehose delivery stream.
|
|
23
|
+
|
|
24
|
+
Three counters report what was not delivered: ``failed`` (the delivery stream told us these
|
|
25
|
+
failed, and they still failed after ``max_retries``), ``dropped_oversized`` (too large for the
|
|
26
|
+
per-record limit to ever accept), and ``dropped_unadjudicated`` (a ``put_record_batch`` response
|
|
27
|
+
whose ``RequestResponses`` did not describe the chunk that was sent, so no record in it could be
|
|
28
|
+
paired to an outcome). A non-zero ``dropped_unadjudicated`` means those records were abandoned
|
|
29
|
+
without the stream ever confirming them — treat it as loss, and as a sign the client is not
|
|
30
|
+
AWS-shaped.
|
|
31
|
+
"""
|
|
22
32
|
|
|
23
33
|
MAX_RECORDS = 500 # put_record_batch hard limit: records per request
|
|
24
34
|
MAX_REQUEST_BYTES = 4 * 1024 * 1024 # 4 MB per put_record_batch request
|
|
@@ -34,6 +44,7 @@ class FirehoseSink:
|
|
|
34
44
|
self.max_retries = max_retries
|
|
35
45
|
self.failed = 0
|
|
36
46
|
self.dropped_oversized = 0
|
|
47
|
+
self.dropped_unadjudicated = 0
|
|
37
48
|
|
|
38
49
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
39
50
|
"""Re-chunk to put_record_batch limits and send each chunk, retrying failures (FR-004)."""
|
|
@@ -74,14 +85,17 @@ class FirehoseSink:
|
|
|
74
85
|
)
|
|
75
86
|
if not response.get("FailedPutCount"):
|
|
76
87
|
return
|
|
77
|
-
results = response.get("RequestResponses"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
results = usable_results(response.get("RequestResponses"))
|
|
89
|
+
verdict = adjudicate_positional(records, results)
|
|
90
|
+
if verdict.unadjudicated:
|
|
91
|
+
self.dropped_unadjudicated += verdict.unadjudicated
|
|
92
|
+
sys.stderr.write(
|
|
93
|
+
f"log-foundry: FirehoseSink could not adjudicate a put_record_batch response "
|
|
94
|
+
f"({len(records)} record(s) sent, {len(results)} result(s) returned); "
|
|
95
|
+
f"{verdict.unadjudicated} record(s) abandoned\n"
|
|
96
|
+
)
|
|
97
|
+
return
|
|
98
|
+
records = verdict.retry
|
|
85
99
|
if not records:
|
|
86
100
|
return
|
|
87
101
|
if attempt >= self.max_retries:
|
|
@@ -13,13 +13,22 @@ import json
|
|
|
13
13
|
import sys
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
+
from log_foundry.sinks._batch import adjudicate_positional, usable_results
|
|
16
17
|
from log_foundry.sinks._chunk import chunk_items
|
|
17
18
|
|
|
18
19
|
__all__ = ["KinesisSink"]
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class KinesisSink:
|
|
22
|
-
"""A :class:`~log_foundry.sinks.base.Sink` that writes events to a Kinesis Data Stream.
|
|
23
|
+
"""A :class:`~log_foundry.sinks.base.Sink` that writes events to a Kinesis Data Stream.
|
|
24
|
+
|
|
25
|
+
Three counters report what was not delivered: ``failed`` (the stream told us these failed, and
|
|
26
|
+
they still failed after ``max_retries``), ``dropped_oversized`` (too large for the per-record
|
|
27
|
+
limit to ever accept), and ``dropped_unadjudicated`` (a ``put_records`` response whose results
|
|
28
|
+
array did not describe the chunk that was sent, so no record in it could be paired to an
|
|
29
|
+
outcome). A non-zero ``dropped_unadjudicated`` means those records were abandoned without the
|
|
30
|
+
stream ever confirming them — treat it as loss, and as a sign the client is not AWS-shaped.
|
|
31
|
+
"""
|
|
23
32
|
|
|
24
33
|
MAX_RECORDS = 500 # put_records hard limit: records per request
|
|
25
34
|
MAX_REQUEST_BYTES = 5 * 1024 * 1024 # 5 MB per put_records request
|
|
@@ -43,6 +52,7 @@ class KinesisSink:
|
|
|
43
52
|
self.max_retries = max_retries
|
|
44
53
|
self.failed = 0
|
|
45
54
|
self.dropped_oversized = 0
|
|
55
|
+
self.dropped_unadjudicated = 0
|
|
46
56
|
|
|
47
57
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
48
58
|
"""Re-chunk to put_records limits and send each chunk, retrying failures (FR-003)."""
|
|
@@ -82,17 +92,17 @@ class KinesisSink:
|
|
|
82
92
|
response = self.client.put_records(StreamName=self.stream_name, Records=records)
|
|
83
93
|
if not response.get("FailedRecordCount"):
|
|
84
94
|
return
|
|
85
|
-
results = response.get("Records"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
results = usable_results(response.get("Records"))
|
|
96
|
+
verdict = adjudicate_positional(records, results)
|
|
97
|
+
if verdict.unadjudicated:
|
|
98
|
+
self.dropped_unadjudicated += verdict.unadjudicated
|
|
99
|
+
sys.stderr.write(
|
|
100
|
+
f"log-foundry: KinesisSink could not adjudicate a put_records response "
|
|
101
|
+
f"({len(records)} record(s) sent, {len(results)} result(s) returned); "
|
|
102
|
+
f"{verdict.unadjudicated} record(s) abandoned\n"
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
records = verdict.retry
|
|
96
106
|
if not records:
|
|
97
107
|
return
|
|
98
108
|
if attempt >= self.max_retries:
|
|
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
|
|
File without changes
|