lumera 0.40.0__tar.gz → 0.41.0.dev1__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.
- {lumera-0.40.0 → lumera-0.41.0.dev1}/PKG-INFO +1 -1
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/_utils.py +38 -18
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera.egg-info/PKG-INFO +1 -1
- {lumera-0.40.0 → lumera-0.41.0.dev1}/pyproject.toml +1 -1
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_sdk.py +100 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/__init__.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/_live.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/agents.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/automations.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/email.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/exceptions.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/files.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/flags.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/function_resources.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/__init__.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/__main__.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_artifacts.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_config.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_context.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_contract.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_dependencies.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_errors.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_execution.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_jsonschema.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_registry.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_remote.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_runner.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/_runtime.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/functions/testing.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/google.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/integrations/__init__.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/integrations/bill.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/integrations/finch.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/integrations/google.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/integrations/kafka.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/integrations/netsuite.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/integrations/salesforce.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/integrations/slack.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/integrations/snowflake.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/llm.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/pb.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/__init__.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/__main__.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_api_child.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_archive.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_builder.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_config.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_contracts.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_errors.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_function_child.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_projection.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_runtime.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_runtime_child.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_schemas.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_target.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/releases/_worker_child.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/sdk.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/storage.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera/webhooks.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera.egg-info/SOURCES.txt +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera.egg-info/dependency_links.txt +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera.egg-info/requires.txt +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/lumera.egg-info/top_level.txt +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/setup.cfg +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_agents_live.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_automations.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_flags.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_function_resources.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_integrations_bill.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_integrations_finch.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_integrations_google.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_integrations_kafka.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_integrations_netsuite.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_integrations_salesforce.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_integrations_snowflake.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_lazy_imports.py +0 -0
- {lumera-0.40.0 → lumera-0.41.0.dev1}/tests/test_webhooks.py +0 -0
|
@@ -205,7 +205,7 @@ def _parse_expiry(expires_at: int | float | str | None) -> float:
|
|
|
205
205
|
raise TypeError(f"Unsupported expires_at format: {type(expires_at)!r}")
|
|
206
206
|
|
|
207
207
|
|
|
208
|
-
def _fetch_access_token(provider: str) -> tuple[str, float]:
|
|
208
|
+
def _fetch_access_token(provider: str) -> tuple[str, float, str]:
|
|
209
209
|
provider = provider.lower().strip()
|
|
210
210
|
if not provider:
|
|
211
211
|
raise ValueError("provider is required")
|
|
@@ -218,15 +218,16 @@ def _fetch_access_token(provider: str) -> tuple[str, float]:
|
|
|
218
218
|
data = resp.json()
|
|
219
219
|
access_token = data.get("access_token")
|
|
220
220
|
expires_at = data.get("expires_at")
|
|
221
|
+
credential_scope = str(data.get("credential_scope") or "")
|
|
221
222
|
|
|
222
223
|
if not access_token:
|
|
223
224
|
raise RuntimeError(f"Malformed response from Lumera when fetching {provider} access token")
|
|
224
225
|
|
|
225
226
|
expiry_ts = _parse_expiry(expires_at)
|
|
226
|
-
return access_token, expiry_ts
|
|
227
|
+
return access_token, expiry_ts, credential_scope
|
|
227
228
|
|
|
228
229
|
|
|
229
|
-
def get_access_token(provider: str, min_valid_seconds: int = 900) -> str:
|
|
230
|
+
def get_access_token(provider: str, min_valid_seconds: int = 900, *, cache: bool = True) -> str:
|
|
230
231
|
"""Get the raw access token string for *provider*.
|
|
231
232
|
|
|
232
233
|
.. deprecated::
|
|
@@ -239,8 +240,8 @@ def get_access_token(provider: str, min_valid_seconds: int = 900) -> str:
|
|
|
239
240
|
providers this is the token itself; for basic-auth providers it is the
|
|
240
241
|
base64-encoded ``username:password``.
|
|
241
242
|
|
|
242
|
-
|
|
243
|
-
|
|
243
|
+
Shared results are cached in-process; delegated results are never cached.
|
|
244
|
+
Pass ``cache=False`` to bypass and avoid populating shared caches.
|
|
244
245
|
"""
|
|
245
246
|
global _token_cache
|
|
246
247
|
|
|
@@ -250,19 +251,22 @@ def get_access_token(provider: str, min_valid_seconds: int = 900) -> str:
|
|
|
250
251
|
|
|
251
252
|
now = _time.time()
|
|
252
253
|
|
|
253
|
-
cached = _token_cache.get(provider)
|
|
254
|
+
cached = _token_cache.get(provider) if cache else None
|
|
254
255
|
if cached is not None:
|
|
255
256
|
access_token, expiry_ts = cached
|
|
256
257
|
if (expiry_ts - now) >= min_valid_seconds:
|
|
257
258
|
return access_token
|
|
258
259
|
|
|
259
|
-
access_token, expiry_ts = _fetch_access_token(provider)
|
|
260
|
-
|
|
260
|
+
access_token, expiry_ts, credential_scope = _fetch_access_token(provider)
|
|
261
|
+
if credential_scope == "delegated":
|
|
262
|
+
_evict_credential_caches(provider)
|
|
263
|
+
elif cache:
|
|
264
|
+
_token_cache[provider] = (access_token, expiry_ts)
|
|
261
265
|
return access_token
|
|
262
266
|
|
|
263
267
|
|
|
264
|
-
def get_google_access_token(min_valid_seconds: int = 900) -> str:
|
|
265
|
-
return get_access_token("google", min_valid_seconds=min_valid_seconds)
|
|
268
|
+
def get_google_access_token(min_valid_seconds: int = 900, *, cache: bool = True) -> str:
|
|
269
|
+
return get_access_token("google", min_valid_seconds=min_valid_seconds, cache=cache)
|
|
266
270
|
|
|
267
271
|
|
|
268
272
|
@dataclass(frozen=True)
|
|
@@ -305,6 +309,9 @@ class Credentials:
|
|
|
305
309
|
extra_headers: dict[str, str] | None = None
|
|
306
310
|
"""Additional provider-required HTTP headers."""
|
|
307
311
|
|
|
312
|
+
credential_scope: str = ""
|
|
313
|
+
"""Credential isolation mode. ``"delegated"`` values are never cached."""
|
|
314
|
+
|
|
308
315
|
def as_header_dict(self) -> dict[str, str]:
|
|
309
316
|
"""Return headers suitable for ``requests.get(headers=...)``."""
|
|
310
317
|
headers = {self.header_name: self.header_value}
|
|
@@ -316,6 +323,13 @@ class Credentials:
|
|
|
316
323
|
_credentials_cache: dict[tuple[str, str], tuple[Credentials, float]] = {}
|
|
317
324
|
|
|
318
325
|
|
|
326
|
+
def _evict_credential_caches(api_name: str) -> None:
|
|
327
|
+
_token_cache.pop(api_name, None)
|
|
328
|
+
for key in tuple(_credentials_cache):
|
|
329
|
+
if key[0] == api_name:
|
|
330
|
+
_credentials_cache.pop(key, None)
|
|
331
|
+
|
|
332
|
+
|
|
319
333
|
def _fetch_credentials(api_name: str, provider: str | None = None) -> Credentials:
|
|
320
334
|
api_name = api_name.lower().strip()
|
|
321
335
|
if not api_name:
|
|
@@ -340,6 +354,7 @@ def _fetch_credentials(api_name: str, provider: str | None = None) -> Credential
|
|
|
340
354
|
# Collect provider-specific extra fields (anything beyond the standard set).
|
|
341
355
|
_standard_keys = {
|
|
342
356
|
"access_token",
|
|
357
|
+
"credential_scope",
|
|
343
358
|
"auth_type",
|
|
344
359
|
"header_name",
|
|
345
360
|
"header_value",
|
|
@@ -362,6 +377,7 @@ def _fetch_credentials(api_name: str, provider: str | None = None) -> Credential
|
|
|
362
377
|
token=access_token,
|
|
363
378
|
provider=str(data.get("provider") or provider_hint or api_name),
|
|
364
379
|
header_name=data.get("header_name", "Authorization"),
|
|
380
|
+
credential_scope=str(data.get("credential_scope") or ""),
|
|
365
381
|
header_value=data.get("header_value", f"Bearer {access_token}"),
|
|
366
382
|
username=data.get("username"),
|
|
367
383
|
expires_at=expiry_ts if expiry_ts != float("inf") else None,
|
|
@@ -374,14 +390,16 @@ def get_credentials(
|
|
|
374
390
|
api_name: str | None = None,
|
|
375
391
|
provider: str | None = None,
|
|
376
392
|
min_valid_seconds: int = 900,
|
|
393
|
+
*,
|
|
394
|
+
cache: bool = True,
|
|
377
395
|
) -> Credentials:
|
|
378
396
|
"""Get a :class:`Credentials` object for *api_name*.
|
|
379
397
|
|
|
380
398
|
This is the recommended way to obtain integration credentials. The
|
|
381
399
|
returned object includes the auth type, ready-to-use HTTP headers, and
|
|
382
|
-
(where applicable) the username.
|
|
383
|
-
|
|
384
|
-
|
|
400
|
+
(where applicable) the username. Shared results are cached in-process;
|
|
401
|
+
delegated results are never cached. Pass ``cache=False`` to bypass and
|
|
402
|
+
avoid populating shared caches.
|
|
385
403
|
|
|
386
404
|
Works with any auth type configured in the Lumera integrations page
|
|
387
405
|
(bearer token, basic auth, etc.).
|
|
@@ -432,7 +450,7 @@ def get_credentials(
|
|
|
432
450
|
|
|
433
451
|
now = _time.time()
|
|
434
452
|
cache_key = (api_name, provider_hint)
|
|
435
|
-
cached = _credentials_cache.get(cache_key)
|
|
453
|
+
cached = _credentials_cache.get(cache_key) if cache else None
|
|
436
454
|
if cached is not None:
|
|
437
455
|
creds, expiry_ts = cached
|
|
438
456
|
if (expiry_ts - now) >= min_valid_seconds:
|
|
@@ -440,10 +458,12 @@ def get_credentials(
|
|
|
440
458
|
|
|
441
459
|
creds = _fetch_credentials(api_name, provider=provider_hint or None)
|
|
442
460
|
expiry_ts = creds.expires_at if creds.expires_at is not None else float("inf")
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
461
|
+
if creds.credential_scope == "delegated":
|
|
462
|
+
_evict_credential_caches(api_name)
|
|
463
|
+
elif cache:
|
|
464
|
+
_credentials_cache[cache_key] = (creds, expiry_ts)
|
|
465
|
+
# Keep the legacy _token_cache in sync so get_access_token stays fast.
|
|
466
|
+
_token_cache[api_name] = (creds.token, expiry_ts)
|
|
447
467
|
|
|
448
468
|
return creds
|
|
449
469
|
|
|
@@ -648,6 +648,106 @@ def test_get_credentials_cache_key_includes_provider_hint(
|
|
|
648
648
|
assert calls == [{"provider": "slack"}, {"provider": "teams"}]
|
|
649
649
|
|
|
650
650
|
|
|
651
|
+
def test_delegated_credentials_are_never_cached(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
652
|
+
monkeypatch.setenv(sdk.TOKEN_ENV, "tok")
|
|
653
|
+
_utils._credentials_cache.clear()
|
|
654
|
+
_utils._token_cache.clear()
|
|
655
|
+
calls = 0
|
|
656
|
+
|
|
657
|
+
class MockSession:
|
|
658
|
+
def get(self, url: str, **kwargs: object) -> DummyResponse:
|
|
659
|
+
nonlocal calls
|
|
660
|
+
del url, kwargs
|
|
661
|
+
calls += 1
|
|
662
|
+
return DummyResponse(
|
|
663
|
+
status_code=200,
|
|
664
|
+
json_data={
|
|
665
|
+
"access_token": f"member-token-{calls}",
|
|
666
|
+
"provider": "google",
|
|
667
|
+
"credential_scope": "delegated",
|
|
668
|
+
"expires_at": "2030-01-01T00:00:00Z",
|
|
669
|
+
},
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
monkeypatch.setattr(_utils, "_get_session", lambda: MockSession())
|
|
673
|
+
|
|
674
|
+
tokens = [_utils.get_credentials("google_drive", provider="google").token for _ in range(3)]
|
|
675
|
+
|
|
676
|
+
assert tokens == ["member-token-1", "member-token-2", "member-token-3"]
|
|
677
|
+
assert calls == 3
|
|
678
|
+
assert _utils._credentials_cache == {}
|
|
679
|
+
assert _utils._token_cache == {}
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def test_cache_false_bootstraps_and_evicts_delegated_credentials(
|
|
683
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
684
|
+
) -> None:
|
|
685
|
+
monkeypatch.setenv(sdk.TOKEN_ENV, "tok")
|
|
686
|
+
_utils._credentials_cache.clear()
|
|
687
|
+
_utils._token_cache.clear()
|
|
688
|
+
responses = ["", "delegated", "delegated"]
|
|
689
|
+
calls = 0
|
|
690
|
+
|
|
691
|
+
class MockSession:
|
|
692
|
+
def get(self, url: str, **kwargs: object) -> DummyResponse:
|
|
693
|
+
nonlocal calls
|
|
694
|
+
del url, kwargs
|
|
695
|
+
scope = responses[calls]
|
|
696
|
+
calls += 1
|
|
697
|
+
return DummyResponse(
|
|
698
|
+
status_code=200,
|
|
699
|
+
json_data={
|
|
700
|
+
"access_token": f"token-{calls}",
|
|
701
|
+
"provider": "google",
|
|
702
|
+
"credential_scope": scope,
|
|
703
|
+
"expires_at": "2030-01-01T00:00:00Z",
|
|
704
|
+
},
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
monkeypatch.setattr(_utils, "_get_session", lambda: MockSession())
|
|
708
|
+
|
|
709
|
+
shared = _utils.get_credentials("google_drive", provider="google")
|
|
710
|
+
delegated = _utils.get_credentials("google_drive", provider="google", cache=False)
|
|
711
|
+
fetched_again = _utils.get_credentials("google_drive", provider="google")
|
|
712
|
+
|
|
713
|
+
assert shared.credential_scope == ""
|
|
714
|
+
assert delegated.credential_scope == "delegated"
|
|
715
|
+
assert fetched_again.token == "token-3"
|
|
716
|
+
assert calls == 3
|
|
717
|
+
assert _utils._credentials_cache == {}
|
|
718
|
+
assert _utils._token_cache == {}
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def test_delegated_legacy_access_tokens_are_never_cached(
|
|
722
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
723
|
+
) -> None:
|
|
724
|
+
monkeypatch.setenv(sdk.TOKEN_ENV, "tok")
|
|
725
|
+
_utils._credentials_cache.clear()
|
|
726
|
+
_utils._token_cache.clear()
|
|
727
|
+
calls = 0
|
|
728
|
+
|
|
729
|
+
class MockSession:
|
|
730
|
+
def get(self, url: str, **kwargs: object) -> DummyResponse:
|
|
731
|
+
nonlocal calls
|
|
732
|
+
del url, kwargs
|
|
733
|
+
calls += 1
|
|
734
|
+
return DummyResponse(
|
|
735
|
+
status_code=200,
|
|
736
|
+
json_data={
|
|
737
|
+
"access_token": f"legacy-{calls}",
|
|
738
|
+
"credential_scope": "delegated",
|
|
739
|
+
"expires_at": "2030-01-01T00:00:00Z",
|
|
740
|
+
},
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
monkeypatch.setattr(_utils, "_get_session", lambda: MockSession())
|
|
744
|
+
|
|
745
|
+
assert _utils.get_access_token("google") == "legacy-1"
|
|
746
|
+
assert _utils.get_access_token("google") == "legacy-2"
|
|
747
|
+
assert calls == 2
|
|
748
|
+
assert _utils._token_cache == {}
|
|
749
|
+
|
|
750
|
+
|
|
651
751
|
def test_get_credentials_preserves_positional_min_valid_seconds(
|
|
652
752
|
monkeypatch: pytest.MonkeyPatch,
|
|
653
753
|
) -> None:
|
|
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
|
|
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
|