botversion-sdk 2.1.0__tar.gz → 2.1.1__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.
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/PKG-INFO +1 -1
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk/interceptor.py +130 -123
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk.egg-info/PKG-INFO +1 -1
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/setup.py +1 -1
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk/__init__.py +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk/bin/__init__.py +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk/bin/scan_endpoints.py +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk/client.py +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk/scanner.py +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk.egg-info/SOURCES.txt +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk.egg-info/dependency_links.txt +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk.egg-info/entry_points.txt +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk.egg-info/requires.txt +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/botversion_sdk.egg-info/top_level.txt +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/pyproject.toml +0 -0
- {botversion_sdk-2.1.0 → botversion_sdk-2.1.1}/setup.cfg +0 -0
|
@@ -119,11 +119,75 @@ def body_structure_to_json_schema(body_structure):
|
|
|
119
119
|
return {"type": "object", "properties": properties}
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
def
|
|
122
|
+
def unwrap_trpc_json_envelope(obj):
|
|
123
|
+
"""
|
|
124
|
+
Unwraps a single (non-batched) tRPC/superjson envelope:
|
|
125
|
+
{ "json": {...realFields...} } -> {...realFields...}
|
|
126
|
+
"""
|
|
127
|
+
if isinstance(obj, dict) and isinstance(obj.get("json"), dict):
|
|
128
|
+
return obj["json"]
|
|
129
|
+
return obj
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def split_batch_path(raw_path):
|
|
133
|
+
"""
|
|
134
|
+
tRPC batches multiple procedure calls into one URL, e.g.
|
|
135
|
+
/api/trpc/me.get,getUserTopBanners,bookingUnconfirmedCount
|
|
136
|
+
tRPC is always mounted at a fixed base containing "/trpc/". Everything
|
|
137
|
+
after that marker is the comma-separated procedure list, and each
|
|
138
|
+
comma-separated piece is ALREADY a complete procedure path on its own
|
|
139
|
+
(it may itself contain "/" for nested routers) — it must never have
|
|
140
|
+
another procedure's prefix re-attached to it.
|
|
141
|
+
Returns a single-item list unchanged if the path isn't a batch.
|
|
142
|
+
"""
|
|
143
|
+
marker = "/trpc/"
|
|
144
|
+
idx = raw_path.find(marker)
|
|
145
|
+
if idx == -1:
|
|
146
|
+
return [raw_path]
|
|
147
|
+
|
|
148
|
+
base = raw_path[: idx + len(marker)]
|
|
149
|
+
tail = raw_path[idx + len(marker):]
|
|
150
|
+
|
|
151
|
+
if "," not in tail:
|
|
152
|
+
return [raw_path]
|
|
153
|
+
|
|
154
|
+
procs = [p.strip() for p in tail.split(",") if p.strip()]
|
|
155
|
+
return [base + p for p in procs]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def split_batch_body(body_obj):
|
|
159
|
+
"""
|
|
160
|
+
Splits a batched tRPC request body/input — {"0": {...}, "1": {...}} —
|
|
161
|
+
into a list of individual bodies, in the same order as the batch keys,
|
|
162
|
+
which lines up with the order split_batch_path() returns procedure
|
|
163
|
+
names in. Each slot is unwrapped from its own {"json": {...}} envelope.
|
|
164
|
+
Returns a single-item list unchanged if the body isn't actually batched.
|
|
165
|
+
"""
|
|
166
|
+
if not isinstance(body_obj, dict):
|
|
167
|
+
return [body_obj]
|
|
168
|
+
|
|
169
|
+
keys = list(body_obj.keys())
|
|
170
|
+
is_batch = len(keys) > 0 and all(k.isdigit() for k in keys)
|
|
171
|
+
|
|
172
|
+
if not is_batch:
|
|
173
|
+
return [body_obj]
|
|
174
|
+
|
|
175
|
+
ordered_keys = sorted(keys, key=lambda k: int(k))
|
|
176
|
+
slots = []
|
|
177
|
+
for k in ordered_keys:
|
|
178
|
+
entry = body_obj[k]
|
|
179
|
+
slots.append(
|
|
180
|
+
unwrap_trpc_json_envelope(entry) if isinstance(entry, dict) else entry
|
|
181
|
+
)
|
|
182
|
+
return slots
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def extract_trpc_get_input_raw(path, query_string):
|
|
123
186
|
"""
|
|
124
187
|
tRPC GET requests send their input as a URL-encoded JSON query parameter
|
|
125
|
-
called 'input'. This
|
|
126
|
-
|
|
188
|
+
called 'input'. This returns the raw decoded dict as-is — still batched
|
|
189
|
+
if applicable — WITHOUT merging different procedures' fields together.
|
|
190
|
+
Splitting per-procedure happens later in report_endpoint().
|
|
127
191
|
Safe for all non-tRPC endpoints — returns None if not applicable.
|
|
128
192
|
"""
|
|
129
193
|
if "/trpc/" not in path:
|
|
@@ -142,41 +206,18 @@ def extract_trpc_get_input(path, query_string):
|
|
|
142
206
|
if not isinstance(decoded, dict):
|
|
143
207
|
return None
|
|
144
208
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
# Format 1 — batched: { "0": { "json": {...} }, "1": { "json": {...} } }
|
|
148
|
-
is_batch = len(keys) > 0 and all(k.isdigit() for k in keys)
|
|
149
|
-
if is_batch:
|
|
150
|
-
merged = {}
|
|
151
|
-
for k in keys:
|
|
152
|
-
entry = decoded[k]
|
|
153
|
-
if isinstance(entry, dict):
|
|
154
|
-
# Unwrap { json: {...} } envelope if present
|
|
155
|
-
unwrapped = entry.get("json", entry) if isinstance(entry.get("json"), dict) else entry
|
|
156
|
-
merged.update(unwrapped)
|
|
157
|
-
return merged if merged else None
|
|
158
|
-
|
|
159
|
-
# Format 2 — single with superjson: { "json": {...}, "meta": {...} }
|
|
160
|
-
if "json" in decoded and isinstance(decoded["json"], dict):
|
|
161
|
-
return decoded["json"]
|
|
162
|
-
|
|
163
|
-
# Format 3 — already unwrapped: { "group": {...}, "limit": 10 }
|
|
164
|
-
if "meta" not in decoded and "json" not in decoded:
|
|
165
|
-
return decoded
|
|
209
|
+
return decoded
|
|
166
210
|
|
|
167
211
|
except Exception:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return None
|
|
212
|
+
return None
|
|
171
213
|
|
|
172
214
|
|
|
173
|
-
def
|
|
215
|
+
def _report_single_endpoint(client, method, normalized_path, body_structure, options):
|
|
174
216
|
"""
|
|
175
|
-
|
|
217
|
+
Reports exactly one already-normalized, already-split endpoint.
|
|
176
218
|
Uses body-key deduplication.
|
|
177
219
|
"""
|
|
178
|
-
|
|
179
|
-
endpoint_key = f"{method}:{normalized}"
|
|
220
|
+
endpoint_key = f"{method}:{normalized_path}"
|
|
180
221
|
|
|
181
222
|
body_fields = sorted(body_structure.keys()) if body_structure else []
|
|
182
223
|
body_key = endpoint_key + ":" + ",".join(body_fields)
|
|
@@ -193,7 +234,7 @@ def report_endpoint(client, method, path, body_structure, options):
|
|
|
193
234
|
try:
|
|
194
235
|
client.update_endpoint({
|
|
195
236
|
"method": method,
|
|
196
|
-
"path":
|
|
237
|
+
"path": normalized_path,
|
|
197
238
|
"request_body": json_schema,
|
|
198
239
|
"detected_by": "runtime",
|
|
199
240
|
})
|
|
@@ -204,6 +245,38 @@ def report_endpoint(client, method, path, body_structure, options):
|
|
|
204
245
|
t.start()
|
|
205
246
|
|
|
206
247
|
|
|
248
|
+
def report_endpoint(client, method, path, body_data, query_string, options):
|
|
249
|
+
"""
|
|
250
|
+
Splits a (possibly batched) tRPC path into one clean endpoint per real
|
|
251
|
+
procedure — instead of one garbled, comma-joined path — with each
|
|
252
|
+
procedure's own body fields only, never mixed with another procedure's.
|
|
253
|
+
Falls back to reporting a single endpoint unchanged when the path
|
|
254
|
+
isn't a batch.
|
|
255
|
+
|
|
256
|
+
body_data — the raw parsed JSON body (or None/empty for GET requests).
|
|
257
|
+
query_string — the raw query string, used to recover tRPC GET input.
|
|
258
|
+
"""
|
|
259
|
+
split_paths = [normalize_path(p) for p in split_batch_path(path)]
|
|
260
|
+
|
|
261
|
+
body_slots = None
|
|
262
|
+
|
|
263
|
+
# tRPC GET requests carry their input in the query string, not the body
|
|
264
|
+
if (not body_data) and query_string:
|
|
265
|
+
raw_input = extract_trpc_get_input_raw(path, query_string)
|
|
266
|
+
if raw_input is not None:
|
|
267
|
+
body_slots = split_batch_body(raw_input)
|
|
268
|
+
|
|
269
|
+
if body_slots is None:
|
|
270
|
+
body_slots = (
|
|
271
|
+
split_batch_body(body_data) if len(split_paths) > 1 else [body_data]
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
for i, single_path in enumerate(split_paths):
|
|
275
|
+
slot_body = body_slots[i] if i < len(body_slots) else None
|
|
276
|
+
body_structure = build_body_structure(slot_body)
|
|
277
|
+
_report_single_endpoint(client, method, single_path, body_structure, options)
|
|
278
|
+
|
|
279
|
+
|
|
207
280
|
# ── FastAPI middleware ────────────────────────────────────────────────────────
|
|
208
281
|
|
|
209
282
|
def attach_fastapi_interceptor(app, client, options):
|
|
@@ -250,18 +323,11 @@ def attach_fastapi_interceptor(app, client, options):
|
|
|
250
323
|
return {"type": "http.request", "body": body_bytes}
|
|
251
324
|
request._receive = receive
|
|
252
325
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
253
|
-
body_structure = build_body_structure(body_data)
|
|
254
|
-
# tRPC GET: input is in URL query param, not body
|
|
255
|
-
if not body_structure:
|
|
256
|
-
query_string = request.url.query
|
|
257
|
-
trpc_body = extract_trpc_get_input(path, query_string)
|
|
258
|
-
if trpc_body:
|
|
259
|
-
body_structure = build_body_structure(trpc_body)
|
|
260
326
|
except Exception:
|
|
261
|
-
|
|
327
|
+
body_data = None
|
|
262
328
|
|
|
263
329
|
if response.status_code < 500:
|
|
264
|
-
report_endpoint(client, method, path,
|
|
330
|
+
report_endpoint(client, method, path, body_data, request.url.query, options)
|
|
265
331
|
|
|
266
332
|
return response
|
|
267
333
|
|
|
@@ -303,16 +369,11 @@ def attach_flask_interceptor(app, client, options):
|
|
|
303
369
|
return
|
|
304
370
|
|
|
305
371
|
try:
|
|
306
|
-
|
|
307
|
-
# tRPC GET: input is in URL query param, not body
|
|
308
|
-
if not body_structure:
|
|
309
|
-
trpc_body = extract_trpc_get_input(path, flask_request.query_string.decode("utf-8"))
|
|
310
|
-
if trpc_body:
|
|
311
|
-
body_structure = build_body_structure(trpc_body)
|
|
372
|
+
body_data = flask_request.get_json(silent=True)
|
|
312
373
|
except Exception:
|
|
313
|
-
|
|
374
|
+
body_data = None
|
|
314
375
|
|
|
315
|
-
report_endpoint(client, method, path,
|
|
376
|
+
report_endpoint(client, method, path, body_data, flask_request.query_string.decode("utf-8"), options)
|
|
316
377
|
|
|
317
378
|
except ImportError:
|
|
318
379
|
pass
|
|
@@ -356,21 +417,16 @@ class BotVersionDjangoMiddleware:
|
|
|
356
417
|
if not self.__class__._options.get("api_prefix") or path.startswith(self.__class__._options["api_prefix"]):
|
|
357
418
|
try:
|
|
358
419
|
body_data = json.loads(request.body) if request.body else None
|
|
359
|
-
body_structure = build_body_structure(body_data)
|
|
360
|
-
# tRPC GET: input is in URL query param, not body
|
|
361
|
-
if not body_structure:
|
|
362
|
-
trpc_body = extract_trpc_get_input(path, request.META.get("QUERY_STRING", ""))
|
|
363
|
-
if trpc_body:
|
|
364
|
-
body_structure = build_body_structure(trpc_body)
|
|
365
420
|
except Exception:
|
|
366
|
-
|
|
421
|
+
body_data = None
|
|
367
422
|
|
|
368
423
|
if self.__class__._client:
|
|
369
424
|
report_endpoint(
|
|
370
425
|
self.__class__._client,
|
|
371
426
|
method,
|
|
372
427
|
path,
|
|
373
|
-
|
|
428
|
+
body_data,
|
|
429
|
+
request.META.get("QUERY_STRING", ""),
|
|
374
430
|
self.__class__._options,
|
|
375
431
|
)
|
|
376
432
|
|
|
@@ -443,18 +499,11 @@ def attach_starlette_interceptor(app, client, options):
|
|
|
443
499
|
try:
|
|
444
500
|
body_bytes = await request.body()
|
|
445
501
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
446
|
-
body_structure = build_body_structure(body_data)
|
|
447
|
-
# tRPC GET: input is in URL query param, not body
|
|
448
|
-
if not body_structure:
|
|
449
|
-
query_string = request.url.query
|
|
450
|
-
trpc_body = extract_trpc_get_input(path, query_string)
|
|
451
|
-
if trpc_body:
|
|
452
|
-
body_structure = build_body_structure(trpc_body)
|
|
453
502
|
except Exception:
|
|
454
|
-
|
|
503
|
+
body_data = None
|
|
455
504
|
|
|
456
505
|
if response.status_code < 500:
|
|
457
|
-
report_endpoint(client, method, path,
|
|
506
|
+
report_endpoint(client, method, path, body_data, request.url.query, options)
|
|
458
507
|
|
|
459
508
|
return response
|
|
460
509
|
|
|
@@ -496,16 +545,10 @@ def attach_sanic_interceptor(app, client, options):
|
|
|
496
545
|
|
|
497
546
|
try:
|
|
498
547
|
body_data = request.json if request.body else None
|
|
499
|
-
body_structure = build_body_structure(body_data)
|
|
500
|
-
# tRPC GET: input is in URL query param, not body
|
|
501
|
-
if not body_structure:
|
|
502
|
-
trpc_body = extract_trpc_get_input(path, request.query_string)
|
|
503
|
-
if trpc_body:
|
|
504
|
-
body_structure = build_body_structure(trpc_body)
|
|
505
548
|
except Exception:
|
|
506
|
-
|
|
549
|
+
body_data = None
|
|
507
550
|
|
|
508
|
-
report_endpoint(client, method, path,
|
|
551
|
+
report_endpoint(client, method, path, body_data, request.query_string, options)
|
|
509
552
|
|
|
510
553
|
except Exception:
|
|
511
554
|
pass
|
|
@@ -551,19 +594,13 @@ def attach_falcon_interceptor(app, client, options):
|
|
|
551
594
|
try:
|
|
552
595
|
body_bytes = req.bounded_stream.read()
|
|
553
596
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
554
|
-
body_structure = build_body_structure(body_data)
|
|
555
|
-
# tRPC GET: input is in URL query param, not body
|
|
556
|
-
if not body_structure:
|
|
557
|
-
trpc_body = extract_trpc_get_input(path, req.query_string)
|
|
558
|
-
if trpc_body:
|
|
559
|
-
body_structure = build_body_structure(trpc_body)
|
|
560
597
|
# Put body back so the actual handler can still read it
|
|
561
598
|
import io
|
|
562
599
|
req.bounded_stream = io.BytesIO(body_bytes)
|
|
563
600
|
except Exception:
|
|
564
|
-
|
|
601
|
+
body_data = None
|
|
565
602
|
|
|
566
|
-
report_endpoint(client, method, path,
|
|
603
|
+
report_endpoint(client, method, path, body_data, req.query_string, options)
|
|
567
604
|
|
|
568
605
|
app.add_middleware(BotVersionFalconMiddleware())
|
|
569
606
|
|
|
@@ -617,16 +654,10 @@ def attach_bottle_interceptor(app, client, options):
|
|
|
617
654
|
|
|
618
655
|
try:
|
|
619
656
|
body_data = bottle_request.json
|
|
620
|
-
body_structure = build_body_structure(body_data)
|
|
621
|
-
# tRPC GET: input is in URL query param, not body
|
|
622
|
-
if not body_structure:
|
|
623
|
-
trpc_body = extract_trpc_get_input(path, bottle_request.query_string)
|
|
624
|
-
if trpc_body:
|
|
625
|
-
body_structure = build_body_structure(trpc_body)
|
|
626
657
|
except Exception:
|
|
627
|
-
|
|
658
|
+
body_data = None
|
|
628
659
|
|
|
629
|
-
report_endpoint(client, method, path,
|
|
660
|
+
report_endpoint(client, method, path, body_data, bottle_request.query_string, options)
|
|
630
661
|
|
|
631
662
|
# Install on the specific app instance, not globally
|
|
632
663
|
app.add_hook("before_request", botversion_bottle_interceptor)
|
|
@@ -676,17 +707,11 @@ def attach_aiohttp_interceptor(app, client, options):
|
|
|
676
707
|
try:
|
|
677
708
|
body_bytes = await request.read()
|
|
678
709
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
679
|
-
body_structure = build_body_structure(body_data)
|
|
680
|
-
# tRPC GET: input is in URL query param, not body
|
|
681
|
-
if not body_structure:
|
|
682
|
-
trpc_body = extract_trpc_get_input(path, request.rel_url.query_string)
|
|
683
|
-
if trpc_body:
|
|
684
|
-
body_structure = build_body_structure(trpc_body)
|
|
685
710
|
except Exception:
|
|
686
|
-
|
|
711
|
+
body_data = None
|
|
687
712
|
|
|
688
713
|
if response.status < 500:
|
|
689
|
-
report_endpoint(client, method, path,
|
|
714
|
+
report_endpoint(client, method, path, body_data, request.rel_url.query_string, options)
|
|
690
715
|
|
|
691
716
|
return response
|
|
692
717
|
|
|
@@ -761,17 +786,11 @@ def attach_tornado_interceptor(app, client, options):
|
|
|
761
786
|
try:
|
|
762
787
|
body_bytes = self.request.body
|
|
763
788
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
764
|
-
body_structure = build_body_structure(body_data)
|
|
765
|
-
# tRPC GET: input is in URL query param, not body
|
|
766
|
-
if not body_structure:
|
|
767
|
-
trpc_body = extract_trpc_get_input(path, self.request.query)
|
|
768
|
-
if trpc_body:
|
|
769
|
-
body_structure = build_body_structure(trpc_body)
|
|
770
789
|
except Exception:
|
|
771
|
-
|
|
790
|
+
body_data = None
|
|
772
791
|
|
|
773
792
|
if self.get_status() < 500:
|
|
774
|
-
report_endpoint(client, method, path,
|
|
793
|
+
report_endpoint(client, method, path, body_data, self.request.query, options)
|
|
775
794
|
except Exception:
|
|
776
795
|
pass
|
|
777
796
|
|
|
@@ -822,17 +841,11 @@ def botversion_pyramid_tween_factory(handler, registry):
|
|
|
822
841
|
if not _pyramid_options.get("api_prefix") or path.startswith(_pyramid_options["api_prefix"]):
|
|
823
842
|
try:
|
|
824
843
|
body_data = request.json_body if request.content_length else None
|
|
825
|
-
body_structure = build_body_structure(body_data)
|
|
826
|
-
# tRPC GET: input is in URL query param, not body
|
|
827
|
-
if not body_structure:
|
|
828
|
-
trpc_body = extract_trpc_get_input(path, request.query_string)
|
|
829
|
-
if trpc_body:
|
|
830
|
-
body_structure = build_body_structure(trpc_body)
|
|
831
844
|
except Exception:
|
|
832
|
-
|
|
845
|
+
body_data = None
|
|
833
846
|
|
|
834
847
|
if response.status_int < 500:
|
|
835
|
-
report_endpoint(_pyramid_client, method, path,
|
|
848
|
+
report_endpoint(_pyramid_client, method, path, body_data, request.query_string, _pyramid_options)
|
|
836
849
|
except Exception:
|
|
837
850
|
pass
|
|
838
851
|
|
|
@@ -897,20 +910,14 @@ def attach_cherrypy_interceptor(app, client, options):
|
|
|
897
910
|
try:
|
|
898
911
|
body_bytes = request.body.read() if request.body else None
|
|
899
912
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
900
|
-
body_structure = build_body_structure(body_data)
|
|
901
|
-
# tRPC GET: input is in URL query param, not body
|
|
902
|
-
if not body_structure:
|
|
903
|
-
query_string = request.query_string if hasattr(request, "query_string") else ""
|
|
904
|
-
trpc_body = extract_trpc_get_input(path, query_string)
|
|
905
|
-
if trpc_body:
|
|
906
|
-
body_structure = build_body_structure(trpc_body)
|
|
907
913
|
# Put body back so actual handler can still read it
|
|
908
914
|
import io
|
|
909
915
|
request.body = io.BytesIO(body_bytes or b"")
|
|
910
916
|
except Exception:
|
|
911
|
-
|
|
917
|
+
body_data = None
|
|
912
918
|
|
|
913
|
-
|
|
919
|
+
query_string = request.query_string if hasattr(request, "query_string") else ""
|
|
920
|
+
report_endpoint(client, method, path, body_data, query_string, options)
|
|
914
921
|
|
|
915
922
|
# Register as a CherryPy tool
|
|
916
923
|
cherrypy.tools.botversion = cherrypy.Tool(
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="botversion-sdk",
|
|
5
|
-
version="2.1.
|
|
5
|
+
version="2.1.1",
|
|
6
6
|
description="BotVersion SDK — automatically discover and register your API endpoints",
|
|
7
7
|
long_description=open("README.md").read() if __import__("os").path.exists("README.md") else "",
|
|
8
8
|
long_description_content_type="text/markdown",
|
|
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
|