streamgate 2.0.0__tar.gz → 2.1.0__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.
- {streamgate-2.0.0 → streamgate-2.1.0}/CHANGELOG.md +23 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/PKG-INFO +1 -1
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/__init__.py +1 -1
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/redis_dedup/__init__.py +4 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/redis_dedup/group_cache.py +35 -4
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/redis_dedup/group_carrier.py +31 -2
- {streamgate-2.0.0 → streamgate-2.1.0}/.github/workflows/ci.yml +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/.github/workflows/release.yml +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/.gitignore +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/AGENTS.md +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/CONFIGURATION.md +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/CONTRIBUTING.md +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/LICENSE +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/README.md +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/README.zh-CN.md +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/docker-compose.yml +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/http_probe/README.md +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/http_probe/consume.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/http_probe/health_server.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/http_probe/models.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/http_probe/produce.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/prod_pipeline/README.md +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/prod_pipeline/consume.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/prod_pipeline/health_server.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/prod_pipeline/models.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/prod_pipeline/produce.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/pure_pipeline/README.md +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/pure_pipeline/consume.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/pure_pipeline/models.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/pure_pipeline/produce.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/redis_admission/README.md +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/redis_admission/models.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/redis_admission/produce.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/sqlite_upsert/README.md +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/sqlite_upsert/consume.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/sqlite_upsert/models.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/examples/sqlite_upsert/produce.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/pyproject.toml +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/config.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/consumer/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/consumer/classifier.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/consumer/dlq.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/consumer/loop.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/consumer/options.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/consumer/runner.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/_deps.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/http_probe/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/http_probe/probe_signal.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/mssql_upsert/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/redis_dedup/cache.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/redis_dedup/carrier.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/redis_dedup/config.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/_dialects/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/_dialects/mssql.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/_dialects/sqlite.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/backfill.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/classifier.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/config.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/engines.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/factory.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/upsert.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sqlite_upsert/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/ingest/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/ingest/dedup/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/ingest/dedup/in_memory.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/ingest/producer.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/obs/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/obs/logging.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/obs/metrics.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/protocols.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/resilience/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/resilience/backpressure.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/resilience/health.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/transport/__init__.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/transport/codec.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/transport/kafka.py +0 -0
- {streamgate-2.0.0 → streamgate-2.1.0}/uv.lock +0 -0
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.1.0] - 2026-09-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`RedisGroupDedupCache.confirm_empty(group)`** — an explicit "confirmed
|
|
13
|
+
empty" negative-cache state for group keys. Writes a reserved marker field
|
|
14
|
+
into the group HASH (`_GROUP_EMPTY_MARKER`, TTL follows
|
|
15
|
+
`group_ttl_seconds`, idle-GC); afterwards `group_exists()` is `True` and
|
|
16
|
+
`get_group_fields()` returns `{}`, so enumeration queries stop re-sourcing a
|
|
17
|
+
known-empty group from the DB on every call. `get_identity_meta` /
|
|
18
|
+
`get_group_fields` filter the marker; `group_exists` semantics are unchanged
|
|
19
|
+
("key present ⇒ group state authoritative, possibly empty"). Purely additive
|
|
20
|
+
— no existing signature changed.
|
|
21
|
+
- **`RedisGroupDedupCarrier.load_group_refill(group)`** — public primitive
|
|
22
|
+
exposing the whole-group cold load + backfill path for reuse by enumeration
|
|
23
|
+
(query) sides instead of copying the mechanism: group-level single-flight,
|
|
24
|
+
concurrency gate, chunked backfill and the "only a successful whole-group
|
|
25
|
+
backfill creates the key" invariant are all shared with `admit` stage 3. It
|
|
26
|
+
returns a `GroupLoadOutcome` (`FOUND` / `EMPTY` / `FAILED` / `GATE_FULL`);
|
|
27
|
+
policy (endpoints, response bodies, HTTP 429/502, `source` labels) stays in
|
|
28
|
+
the caller's adapter layer. `GroupLoadOutcome` / `GroupLoadResult` are now
|
|
29
|
+
exported from `streamgate.contrib.redis_dedup`.
|
|
30
|
+
|
|
8
31
|
## [2.0.0] - 2026-09-11
|
|
9
32
|
|
|
10
33
|
### BREAKING — unified backfill contract (feat!)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: streamgate
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A pure-core Kafka data pipeline framework: conditional admission, reliable delivery, and one obvious consume-loop outlet via a typed handler.
|
|
5
5
|
Project-URL: Homepage, https://github.com/pwg-code/streamgate
|
|
6
6
|
Project-URL: Repository, https://github.com/pwg-code/streamgate
|
|
@@ -24,6 +24,8 @@ try:
|
|
|
24
24
|
from streamgate.contrib.redis_dedup.config import RedisConfig
|
|
25
25
|
from streamgate.contrib.redis_dedup.group_cache import RedisGroupDedupCache
|
|
26
26
|
from streamgate.contrib.redis_dedup.group_carrier import (
|
|
27
|
+
GroupLoadOutcome,
|
|
28
|
+
GroupLoadResult,
|
|
27
29
|
RedisGroupDedupCarrier,
|
|
28
30
|
RedisGroupDedupCarrierConfig,
|
|
29
31
|
)
|
|
@@ -32,6 +34,8 @@ except ModuleNotFoundError as e:
|
|
|
32
34
|
|
|
33
35
|
__all__ = [
|
|
34
36
|
"ColdPathGateFullError",
|
|
37
|
+
"GroupLoadOutcome",
|
|
38
|
+
"GroupLoadResult",
|
|
35
39
|
"RedisConfig",
|
|
36
40
|
"RedisDedupCache",
|
|
37
41
|
"RedisDedupCarrier",
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
- 幂等摘要写:HSET + EXPIRE 无条件续期(force / on_persisted / 回填共用)
|
|
7
7
|
- 整组回填 write_group_fields:pipeline 分片写入(分片为纯实现细节,无配置、无上界),
|
|
8
8
|
写后统一 EXPIRE
|
|
9
|
-
- 组存在探测 group_exists:EXISTS。组 key 生命周期不可变:仅"整组回填成功"
|
|
10
|
-
"空组确认后首身份占位"
|
|
9
|
+
- 组存在探测 group_exists:EXISTS。组 key 生命周期不可变:仅"整组回填成功"、
|
|
10
|
+
"空组确认后首身份占位"、"整组确认空标记 confirm_empty"三条路径创建 →
|
|
11
|
+
组 key 存在 ⇒ 组状态可信(含"已确认空"负缓存态),判定快路径照常成立
|
|
12
|
+
- "确认空组"以专用 field 标记(_GROUP_EMPTY_MARKER)落 HASH:枚举查询侧
|
|
13
|
+
group_exists=True 直接零 DB,get_group_fields 过滤该标记返回 {};判重侧
|
|
14
|
+
phase 2 快路径不变(field 缺失 = 组内确认无此身份)
|
|
11
15
|
- fail-closed 与否是判重载体的参数,缓存层只如实报告错误
|
|
12
16
|
"""
|
|
13
17
|
|
|
@@ -29,6 +33,12 @@ _HEALTH_TIMEOUT_S = 2.0
|
|
|
29
33
|
# 整组回填分片大小(纯实现细节:一次 HSET 的 field 数量上限,非业务配置)
|
|
30
34
|
_WRITE_CHUNK_SIZE = 200
|
|
31
35
|
|
|
36
|
+
# "整组确认空"负缓存标记:作为独立 field 写入组 HASH,标记该组已获 DB 确认无记录。
|
|
37
|
+
# 组 key 因此存在(group_exists=True),枚举查询侧不再判"冷"反复整组回源;
|
|
38
|
+
# 判定快路径语义不变(field 缺失 = 组内确认无此身份)。取值需保证不可能与
|
|
39
|
+
# 真实组内身份重名。
|
|
40
|
+
_GROUP_EMPTY_MARKER = "__streamgate_group_empty__"
|
|
41
|
+
|
|
32
42
|
# 原子组内占位(单 key,Cluster 兼容):命中即返回既有摘要,未命中写入 + TTL
|
|
33
43
|
# KEYS[1]=group key, ARGV=[identity, summary_json, ttl_seconds]
|
|
34
44
|
# 返回 [1, ""] = 占位成功;[0, existing_json] = 已被占(竞态输家未写入,不续期)
|
|
@@ -131,7 +141,7 @@ class RedisGroupDedupCache:
|
|
|
131
141
|
self._require_client().hget(self.group_key(group), identity),
|
|
132
142
|
)
|
|
133
143
|
)
|
|
134
|
-
if raw is None:
|
|
144
|
+
if raw is None or identity == _GROUP_EMPTY_MARKER:
|
|
135
145
|
return None
|
|
136
146
|
return parse_summary(str(raw))
|
|
137
147
|
|
|
@@ -142,7 +152,11 @@ class RedisGroupDedupCache:
|
|
|
142
152
|
self._require_client().hgetall(self.group_key(group)),
|
|
143
153
|
)
|
|
144
154
|
)
|
|
145
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
identity: parse_summary(str(payload))
|
|
157
|
+
for identity, payload in raw.items()
|
|
158
|
+
if identity != _GROUP_EMPTY_MARKER
|
|
159
|
+
}
|
|
146
160
|
|
|
147
161
|
async def get_ttl(self, group: str) -> int:
|
|
148
162
|
return int(
|
|
@@ -150,6 +164,8 @@ class RedisGroupDedupCache:
|
|
|
150
164
|
)
|
|
151
165
|
|
|
152
166
|
async def group_exists(self, group: str) -> bool:
|
|
167
|
+
"""组存在探测。组 key 存在 ⇒ 组状态可信(含"已确认空"负缓存态),
|
|
168
|
+
判定快路径照常成立:field 缺失 = DB 内确认无此身份。"""
|
|
153
169
|
hits = int(
|
|
154
170
|
await self._read(self._require_client().exists(self.group_key(group)))
|
|
155
171
|
)
|
|
@@ -208,6 +224,21 @@ class RedisGroupDedupCache:
|
|
|
208
224
|
pipe.expire(key, self._config.group_ttl_seconds)
|
|
209
225
|
await self._write(pipe.execute())
|
|
210
226
|
|
|
227
|
+
async def confirm_empty(self, group: str) -> None:
|
|
228
|
+
"""标记"整组确认空"负缓存态:写专用 marker + EXPIRE(TTL 沿用
|
|
229
|
+
group_ttl_seconds,idle GC)。
|
|
230
|
+
|
|
231
|
+
此后 group_exists() 为 True(键存在 ⇒ 组状态可信,可能为空)且
|
|
232
|
+
get_group_fields() 返回 {},枚举查询侧不再判"冷"反复整组回源。
|
|
233
|
+
判重 phase 2 快路径不变:组内 field 缺失 = DB 确认无此身份 → 占位。
|
|
234
|
+
调用前提由使用方保证:仅在 DB 确认组无记录后调用(写库后无自我失效
|
|
235
|
+
语义)。
|
|
236
|
+
|
|
237
|
+
marker 已被 get_identity_meta / get_group_fields 过滤,不会以真实
|
|
238
|
+
身份或字段泄漏给上层。
|
|
239
|
+
"""
|
|
240
|
+
await self.write_summary(group, _GROUP_EMPTY_MARKER, {})
|
|
241
|
+
|
|
211
242
|
async def delete_group(self, group: str) -> None:
|
|
212
243
|
"""删除整组(供告警/运维删键修复)。"""
|
|
213
244
|
await self._write(self._require_client().delete(self.group_key(group)))
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
- 组键 = HASH(field = 组内身份),以组为单位缓存,整组一次冷回源预热
|
|
5
5
|
- admit 三阶段:组内缓存判定 → 组存在快路径(零 DB)→ 全新组冷回源
|
|
6
6
|
(组级单飞 + 并发闸门)
|
|
7
|
-
- 组 key 生命周期不可变:仅"整组回填成功"
|
|
8
|
-
|
|
7
|
+
- 组 key 生命周期不可变:仅"整组回填成功"、"空组确认后首身份占位"、
|
|
8
|
+
"整组确认空标记(cache.confirm_empty)"三条路径创建 → 组 key 存在 ⇒
|
|
9
|
+
组状态可信(可能为空),无需任何安全阀/开关
|
|
10
|
+
- load_group_refill(group) 公开"整组冷回源 + 回填建键"原语:复用组级单飞 +
|
|
11
|
+
并发闸门 + 分片回填(与 admit 阶段3 同一机制),供枚举查询侧复用避免复制
|
|
9
12
|
|
|
10
13
|
框架保证调用时序:admit → (Kafka 发送) → on_send_success(每次成功,通知型)/
|
|
11
14
|
on_send_failed(每次失败,默认保留占位 TTL 自愈)→ on_force_accepted(仅
|
|
@@ -365,6 +368,31 @@ class RedisGroupDedupCarrier(Generic[RecordModelT]):
|
|
|
365
368
|
mapping=fields,
|
|
366
369
|
)
|
|
367
370
|
|
|
371
|
+
# ---- 公开原语:整组冷回源 + 回填(枚举查询侧复用,避免复制机制)----
|
|
372
|
+
|
|
373
|
+
async def load_group_refill(self, group: str) -> GroupLoadOutcome:
|
|
374
|
+
"""整组冷回源 + 回填建键(公开原语,供枚举查询侧复用)。
|
|
375
|
+
|
|
376
|
+
与 admit 阶段3 复用同一机制与不变式:
|
|
377
|
+
- 组级单飞:同组并发首触只回源一次,Future 共享;等待方不悬挂;
|
|
378
|
+
- 并发闸门:闸门满返回 GATE_FULL 态,不触碰 DB(REJECT 方向);
|
|
379
|
+
- 整组分片回填后统一续期;仅整组回填成功才建键(write_group_fields
|
|
380
|
+
分片写出自动建键),半组/失败不成键、不对外判定。
|
|
381
|
+
|
|
382
|
+
返回 GroupLoadOutcome(三态):
|
|
383
|
+
- FOUND:组内字段已回填,使用方 get_group_fields(group) 直接命中;
|
|
384
|
+
- EMPTY:DB 确认组无记录,使用方可选 cache.confirm_empty(group)
|
|
385
|
+
落"确认空"负缓存,消除后续冷回源;
|
|
386
|
+
- FAILED / GATE_FULL:不建键,由使用方按策略处置。
|
|
387
|
+
|
|
388
|
+
边界:本方法只暴露机制,不纳入枚举查询端点/响应体/HTTP 429-502/
|
|
389
|
+
source 标签等策略内容(mechanism vs policy)。
|
|
390
|
+
"""
|
|
391
|
+
outcome = await self._load_group_single_flight(group)
|
|
392
|
+
if outcome.result is GroupLoadResult.FOUND:
|
|
393
|
+
await self._backfill_best_effort(group, outcome.mapping)
|
|
394
|
+
return outcome
|
|
395
|
+
|
|
368
396
|
# ---- 占位与回填 ----
|
|
369
397
|
|
|
370
398
|
async def _reserve(
|
|
@@ -466,6 +494,7 @@ class RedisGroupDedupCarrier(Generic[RecordModelT]):
|
|
|
466
494
|
|
|
467
495
|
|
|
468
496
|
__all__ = [
|
|
497
|
+
"GroupLoadOutcome",
|
|
469
498
|
"GroupLoadResult",
|
|
470
499
|
"RedisGroupDedupCarrier",
|
|
471
500
|
"RedisGroupDedupCarrierConfig",
|
|
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
|
|
File without changes
|
{streamgate-2.0.0 → streamgate-2.1.0}/src/streamgate/contrib/sql_upsert/_dialects/__init__.py
RENAMED
|
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
|