kstlib 2.2.0__tar.gz → 2.2.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.
- {kstlib-2.2.0/src/kstlib.egg-info → kstlib-2.2.1}/PKG-INFO +2 -2
- {kstlib-2.2.0 → kstlib-2.2.1}/pyproject.toml +1 -1
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/limits.py +39 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/meta.py +1 -1
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/websocket/manager.py +128 -5
- {kstlib-2.2.0 → kstlib-2.2.1/src/kstlib.egg-info}/PKG-INFO +2 -2
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib.egg-info/requires.txt +1 -1
- {kstlib-2.2.0 → kstlib-2.2.1}/LICENSE.md +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/MANIFEST.in +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/README.md +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/setup.cfg +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/__main__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/channels/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/channels/base.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/channels/email.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/channels/slack.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/manager.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/models.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/throttle.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/callback.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/check.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/config.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/errors.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/models.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/providers/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/providers/base.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/providers/oauth2.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/providers/oidc.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/session.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/token.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cache/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cache/decorator.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cache/strategies.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/app.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/check.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/common.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/login.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/logout.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/providers.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/status.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/token.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/whoami.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/config.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/attach.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/common.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/list_sessions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/logs.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/start.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/status.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/stop.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/rapi/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/rapi/call.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/rapi/list.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/rapi/show.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/common.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/decrypt.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/doctor.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/encrypt.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/shred.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/common.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/config/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/config/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/config/export.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/config/loader.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/config/sops.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/aiosqlcipher.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/cipher.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/database.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/pool.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/helpers/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/helpers/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/helpers/time_trigger.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/kstlib.conf.yml +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/logging/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/logging/manager.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/builder.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/filesystem.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transport.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transports/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transports/gmail.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transports/resend.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transports/ses.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transports/smtp.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/metrics/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/metrics/decorators.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/metrics/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/_styles.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/cell.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/config.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/delivery.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/image.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/kv.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/list.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/metric.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/monitoring.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/renderer.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/service.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/table.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/types.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/base.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/container.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/manager.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/models.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/tmux.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/validators.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/base.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/models.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/runner.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/steps/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/steps/_base.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/steps/callable.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/steps/python.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/steps/shell.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/validators.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/py.typed +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/rapi/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/rapi/client.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/rapi/config.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/rapi/credentials.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/rapi/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/circuit_breaker.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/heartbeat.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/rate_limiter.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/shutdown.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/watchdog.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/models.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/base.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/environment.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/keyring.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/kms.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/kwargs.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/sops.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/resolver.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/sensitive.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secure/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secure/fs.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secure/permissions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ssl.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/chain.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/config.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/primitives.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/validators.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ui/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ui/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ui/panels.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ui/spinner.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ui/tables.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/dict.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/formatting.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/http_trace.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/lazy.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/secure_delete.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/serialization.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/text.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/validators.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/websocket/__init__.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/websocket/exceptions.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/websocket/models.py +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib.egg-info/SOURCES.txt +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib.egg-info/dependency_links.txt +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib.egg-info/entry_points.txt +0 -0
- {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kstlib
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Config-driven helpers for Python projects (dynamic config, secure secrets, preset logging, and more…)
|
|
5
5
|
Author-email: Michel TRUONG <michel.truong@gmail.com>
|
|
6
6
|
Maintainer-email: Michel TRUONG <michel.truong@gmail.com>
|
|
@@ -42,7 +42,7 @@ Requires-Dist: websockets<16,>=15.0
|
|
|
42
42
|
Requires-Dist: jinja2<4,>=3.1.5
|
|
43
43
|
Requires-Dist: humanize<5,>=4.11
|
|
44
44
|
Requires-Dist: httpx<1,>=0.28
|
|
45
|
-
Requires-Dist: authlib<2,>=1.6.
|
|
45
|
+
Requires-Dist: authlib<2,>=1.6.11
|
|
46
46
|
Requires-Dist: pendulum<4,>=3.0
|
|
47
47
|
Requires-Dist: cryptography>=46.0.7
|
|
48
48
|
Requires-Dist: requests>=2.33.0
|
|
@@ -56,7 +56,7 @@ dependencies = [
|
|
|
56
56
|
|
|
57
57
|
# --- HTTP Client & Auth ---
|
|
58
58
|
"httpx>=0.28,<1", # Modern async HTTP client (OAuth2/OIDC flows)
|
|
59
|
-
"authlib>=1.6.
|
|
59
|
+
"authlib>=1.6.11,<2", # OAuth2/OIDC client + JWT signature verification (GHSA-jj8c-mmj3-mmgv: CSRF in cache-backed OAuth state)
|
|
60
60
|
|
|
61
61
|
# --- Time & Scheduling ---
|
|
62
62
|
"pendulum>=3.0,<4", # Modern datetime library with timezone support
|
|
@@ -200,6 +200,18 @@ HARD_MAX_WS_RECONNECT_CHECK = 60.0
|
|
|
200
200
|
HARD_MIN_WS_DISCONNECT_MARGIN = 60.0
|
|
201
201
|
HARD_MAX_WS_DISCONNECT_MARGIN = 3600.0
|
|
202
202
|
|
|
203
|
+
#: WebSocket stable connection time bounds (seconds) - delay before resetting reconnect counter.
|
|
204
|
+
HARD_MIN_WS_STABLE_CONNECTION_TIME = 10.0
|
|
205
|
+
HARD_MAX_WS_STABLE_CONNECTION_TIME = 300.0
|
|
206
|
+
|
|
207
|
+
#: WebSocket server unavailable (code 1013) backoff bounds (seconds).
|
|
208
|
+
HARD_MIN_WS_SERVER_UNAVAILABLE_DELAY = 10.0
|
|
209
|
+
HARD_MAX_WS_SERVER_UNAVAILABLE_DELAY = 300.0
|
|
210
|
+
|
|
211
|
+
#: WebSocket disconnect alert interval bounds (seconds) - throttle window for alerts.
|
|
212
|
+
HARD_MIN_WS_DISCONNECT_ALERT_INTERVAL = 30.0
|
|
213
|
+
HARD_MAX_WS_DISCONNECT_ALERT_INTERVAL = 3600.0
|
|
214
|
+
|
|
203
215
|
#: Maximum endpoint reference length (api.endpoint format) - protects against DoS.
|
|
204
216
|
HARD_MAX_ENDPOINT_REF_LENGTH = 256
|
|
205
217
|
|
|
@@ -272,6 +284,9 @@ DEFAULT_WS_QUEUE_SIZE = 1000 # messages
|
|
|
272
284
|
DEFAULT_WS_DISCONNECT_CHECK = 10.0 # seconds
|
|
273
285
|
DEFAULT_WS_RECONNECT_CHECK = 5.0 # seconds
|
|
274
286
|
DEFAULT_WS_DISCONNECT_MARGIN = 300.0 # seconds (5 minutes before 24h limit)
|
|
287
|
+
DEFAULT_WS_STABLE_CONNECTION_TIME = 60.0 # seconds before resetting reconnect counter
|
|
288
|
+
DEFAULT_WS_SERVER_UNAVAILABLE_DELAY = 30.0 # seconds to wait on code 1013
|
|
289
|
+
DEFAULT_WS_DISCONNECT_ALERT_INTERVAL = 300.0 # seconds between throttled alerts
|
|
275
290
|
|
|
276
291
|
DEFAULT_PIPELINE_TIMEOUT = 300.0 # seconds (5 minutes)
|
|
277
292
|
DEFAULT_PIPELINE_ON_ERROR = "fail_fast"
|
|
@@ -925,6 +940,9 @@ class WebSocketLimits:
|
|
|
925
940
|
disconnect_check_interval: Seconds between should_disconnect checks.
|
|
926
941
|
reconnect_check_interval: Seconds between should_reconnect checks.
|
|
927
942
|
disconnect_margin: Seconds before platform limit to disconnect.
|
|
943
|
+
stable_connection_time: Seconds of stable connection before resetting reconnect counter.
|
|
944
|
+
server_unavailable_delay: Seconds to wait on server code 1013 before reconnect.
|
|
945
|
+
disconnect_alert_interval: Seconds between throttled disconnect alerts.
|
|
928
946
|
|
|
929
947
|
"""
|
|
930
948
|
|
|
@@ -938,6 +956,9 @@ class WebSocketLimits:
|
|
|
938
956
|
disconnect_check_interval: float
|
|
939
957
|
reconnect_check_interval: float
|
|
940
958
|
disconnect_margin: float
|
|
959
|
+
stable_connection_time: float
|
|
960
|
+
server_unavailable_delay: float
|
|
961
|
+
disconnect_alert_interval: float
|
|
941
962
|
|
|
942
963
|
|
|
943
964
|
def get_websocket_limits(
|
|
@@ -1023,6 +1044,24 @@ def get_websocket_limits(
|
|
|
1023
1044
|
HARD_MIN_WS_DISCONNECT_MARGIN,
|
|
1024
1045
|
HARD_MAX_WS_DISCONNECT_MARGIN,
|
|
1025
1046
|
),
|
|
1047
|
+
stable_connection_time=_parse_float_config(
|
|
1048
|
+
_get_nested(config, "websocket", "reconnect", "stable_connection_time"),
|
|
1049
|
+
DEFAULT_WS_STABLE_CONNECTION_TIME,
|
|
1050
|
+
HARD_MIN_WS_STABLE_CONNECTION_TIME,
|
|
1051
|
+
HARD_MAX_WS_STABLE_CONNECTION_TIME,
|
|
1052
|
+
),
|
|
1053
|
+
server_unavailable_delay=_parse_float_config(
|
|
1054
|
+
_get_nested(config, "websocket", "reconnect", "server_unavailable_delay"),
|
|
1055
|
+
DEFAULT_WS_SERVER_UNAVAILABLE_DELAY,
|
|
1056
|
+
HARD_MIN_WS_SERVER_UNAVAILABLE_DELAY,
|
|
1057
|
+
HARD_MAX_WS_SERVER_UNAVAILABLE_DELAY,
|
|
1058
|
+
),
|
|
1059
|
+
disconnect_alert_interval=_parse_float_config(
|
|
1060
|
+
_get_nested(config, "websocket", "alert", "disconnect_interval"),
|
|
1061
|
+
DEFAULT_WS_DISCONNECT_ALERT_INTERVAL,
|
|
1062
|
+
HARD_MIN_WS_DISCONNECT_ALERT_INTERVAL,
|
|
1063
|
+
HARD_MAX_WS_DISCONNECT_ALERT_INTERVAL,
|
|
1064
|
+
),
|
|
1026
1065
|
)
|
|
1027
1066
|
|
|
1028
1067
|
|
|
@@ -89,6 +89,9 @@ __all__ = ["WebSocketManager"]
|
|
|
89
89
|
|
|
90
90
|
log = logging.getLogger(__name__)
|
|
91
91
|
|
|
92
|
+
#: WebSocket close code 1013 "Try Again Later" - server temporarily unavailable.
|
|
93
|
+
WS_CODE_TRY_AGAIN_LATER = 1013
|
|
94
|
+
|
|
92
95
|
# Type aliases for callbacks
|
|
93
96
|
ShouldDisconnectCallback = Callable[[], bool]
|
|
94
97
|
ShouldReconnectCallback = Callable[[], bool | float]
|
|
@@ -96,6 +99,7 @@ OnConnectCallback = Callable[[], Awaitable[None] | None]
|
|
|
96
99
|
OnDisconnectCallback = Callable[[DisconnectReason], Awaitable[None] | None]
|
|
97
100
|
OnMessageCallback = Callable[[Any], Awaitable[None] | None]
|
|
98
101
|
OnAlertCallback = Callable[[str, str, Mapping[str, Any]], Awaitable[None] | None]
|
|
102
|
+
DisconnectAlertCallback = Callable[[DisconnectReason, int], Awaitable[None] | None]
|
|
99
103
|
|
|
100
104
|
|
|
101
105
|
def _check_websockets_installed() -> None:
|
|
@@ -152,6 +156,7 @@ class WebSocketManager:
|
|
|
152
156
|
should_reconnect: ShouldReconnectCallback | None = None,
|
|
153
157
|
on_connect: OnConnectCallback | None = None,
|
|
154
158
|
on_disconnect: OnDisconnectCallback | None = None,
|
|
159
|
+
on_disconnect_alert: DisconnectAlertCallback | None = None,
|
|
155
160
|
on_message: OnMessageCallback | None = None,
|
|
156
161
|
on_alert: OnAlertCallback | None = None,
|
|
157
162
|
# Connection settings
|
|
@@ -164,10 +169,14 @@ class WebSocketManager:
|
|
|
164
169
|
max_reconnect_delay: float | None = None,
|
|
165
170
|
max_reconnect_attempts: int | None = None,
|
|
166
171
|
auto_reconnect: bool = True,
|
|
172
|
+
stable_connection_time: float | None = None,
|
|
173
|
+
server_unavailable_delay: float | None = None,
|
|
167
174
|
# Proactive control settings
|
|
168
175
|
disconnect_check_interval: float | None = None,
|
|
169
176
|
reconnect_check_interval: float | None = None,
|
|
170
177
|
disconnect_margin: float | None = None,
|
|
178
|
+
# Alert throttle
|
|
179
|
+
disconnect_alert_interval: float | None = None,
|
|
171
180
|
# Queue settings
|
|
172
181
|
queue_size: int | None = None,
|
|
173
182
|
# Config
|
|
@@ -181,6 +190,10 @@ class WebSocketManager:
|
|
|
181
190
|
should_reconnect: Callback returning True or delay (seconds) for reconnect.
|
|
182
191
|
on_connect: Callback invoked after successful connection.
|
|
183
192
|
on_disconnect: Callback invoked after disconnection with reason.
|
|
193
|
+
on_disconnect_alert: Throttled callback for disconnect alerts.
|
|
194
|
+
Receives (reason, count) where count is the number of
|
|
195
|
+
disconnects since the last alert. Fires at most once per
|
|
196
|
+
``disconnect_alert_interval`` seconds.
|
|
184
197
|
on_message: Callback invoked for each received message.
|
|
185
198
|
on_alert: Callback for alerting (channel, message, context).
|
|
186
199
|
ping_interval: Seconds between ping frames.
|
|
@@ -191,9 +204,17 @@ class WebSocketManager:
|
|
|
191
204
|
max_reconnect_delay: Maximum delay for exponential backoff.
|
|
192
205
|
max_reconnect_attempts: Maximum consecutive reconnection attempts.
|
|
193
206
|
auto_reconnect: Whether to auto-reconnect on disconnection.
|
|
207
|
+
stable_connection_time: Seconds of stable connection required
|
|
208
|
+
before ``_reconnect_count`` is reset to 0. Protects against
|
|
209
|
+
flapping servers that accept the handshake then close.
|
|
210
|
+
server_unavailable_delay: Seconds to wait before any reconnect
|
|
211
|
+
attempt after receiving close code 1013 (Try Again Later).
|
|
194
212
|
disconnect_check_interval: Seconds between should_disconnect checks.
|
|
195
213
|
reconnect_check_interval: Seconds between should_reconnect checks.
|
|
196
214
|
disconnect_margin: Seconds before platform limit to disconnect.
|
|
215
|
+
disconnect_alert_interval: Minimum seconds between calls to
|
|
216
|
+
``on_disconnect_alert``. Aggregated count of skipped
|
|
217
|
+
disconnects is passed to the callback.
|
|
197
218
|
queue_size: Maximum messages in queue (0 = unlimited).
|
|
198
219
|
config: Optional config mapping for limits resolution.
|
|
199
220
|
|
|
@@ -229,6 +250,15 @@ class WebSocketManager:
|
|
|
229
250
|
)
|
|
230
251
|
self._disconnect_margin = disconnect_margin if disconnect_margin is not None else limits.disconnect_margin
|
|
231
252
|
self._queue_size = queue_size if queue_size is not None else limits.queue_size
|
|
253
|
+
self._stable_connection_time = (
|
|
254
|
+
stable_connection_time if stable_connection_time is not None else limits.stable_connection_time
|
|
255
|
+
)
|
|
256
|
+
self._server_unavailable_delay = (
|
|
257
|
+
server_unavailable_delay if server_unavailable_delay is not None else limits.server_unavailable_delay
|
|
258
|
+
)
|
|
259
|
+
self._disconnect_alert_interval = (
|
|
260
|
+
disconnect_alert_interval if disconnect_alert_interval is not None else limits.disconnect_alert_interval
|
|
261
|
+
)
|
|
232
262
|
|
|
233
263
|
# Settings
|
|
234
264
|
self._reconnect_strategy = reconnect_strategy
|
|
@@ -239,6 +269,7 @@ class WebSocketManager:
|
|
|
239
269
|
self._should_reconnect = should_reconnect
|
|
240
270
|
self._on_connect = on_connect
|
|
241
271
|
self._on_disconnect = on_disconnect
|
|
272
|
+
self._on_disconnect_alert = on_disconnect_alert
|
|
242
273
|
self._on_message = on_message
|
|
243
274
|
self._on_alert = on_alert
|
|
244
275
|
|
|
@@ -249,12 +280,16 @@ class WebSocketManager:
|
|
|
249
280
|
self._reconnect_count = 0
|
|
250
281
|
self._connect_time: float = 0.0
|
|
251
282
|
self._scheduled_reconnect_delay: float | None = None
|
|
283
|
+
self._force_backoff_delay: float | None = None
|
|
284
|
+
self._disconnect_alert_count: int = 0
|
|
285
|
+
self._last_disconnect_alert_at: float = 0.0
|
|
252
286
|
|
|
253
287
|
# Background tasks
|
|
254
288
|
self._disconnect_check_task: asyncio.Task[None] | None = None
|
|
255
289
|
self._reconnect_check_task: asyncio.Task[None] | None = None
|
|
256
290
|
self._receive_task: asyncio.Task[None] | None = None
|
|
257
291
|
self._ping_task: asyncio.Task[None] | None = None
|
|
292
|
+
self._stable_connection_task: asyncio.Task[None] | None = None
|
|
258
293
|
|
|
259
294
|
# Events
|
|
260
295
|
self._connected_event = asyncio.Event()
|
|
@@ -398,11 +433,17 @@ class WebSocketManager:
|
|
|
398
433
|
) from last_error
|
|
399
434
|
|
|
400
435
|
async def _finalize_connection(self) -> None:
|
|
401
|
-
"""Finalize successful connection setup.
|
|
436
|
+
"""Finalize successful connection setup.
|
|
437
|
+
|
|
438
|
+
Note: ``_reconnect_count`` is NOT reset here. A successful handshake
|
|
439
|
+
alone is not proof of a healthy connection - a flapping server can
|
|
440
|
+
accept the TCP/WS handshake then close immediately. The counter is
|
|
441
|
+
reset only after ``_stable_connection_time`` seconds of uptime by
|
|
442
|
+
``_stable_connection_reset_loop``.
|
|
443
|
+
"""
|
|
402
444
|
self._state = ConnectionState.CONNECTED
|
|
403
445
|
self._connected_event.set()
|
|
404
446
|
self._connect_time = time.monotonic()
|
|
405
|
-
self._reconnect_count = 0
|
|
406
447
|
self._stats.record_connect()
|
|
407
448
|
|
|
408
449
|
log.info("WebSocket connected to %s", self._url)
|
|
@@ -429,6 +470,31 @@ class WebSocketManager:
|
|
|
429
470
|
if self._should_disconnect is not None:
|
|
430
471
|
self._disconnect_check_task = asyncio.create_task(self._disconnect_check_loop(), name="ws_disconnect_check")
|
|
431
472
|
|
|
473
|
+
# Start delayed reconnect-counter reset. Only scheduled if a previous
|
|
474
|
+
# reconnect has occurred. Avoids scheduling a no-op task on first connect.
|
|
475
|
+
if self._reconnect_count > 0:
|
|
476
|
+
self._stable_connection_task = asyncio.create_task(
|
|
477
|
+
self._stable_connection_reset_loop(),
|
|
478
|
+
name="ws_stable_connection_reset",
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
async def _stable_connection_reset_loop(self) -> None:
|
|
482
|
+
"""Reset ``_reconnect_count`` after ``_stable_connection_time`` seconds.
|
|
483
|
+
|
|
484
|
+
Scheduled from ``_start_background_tasks``. If the connection drops
|
|
485
|
+
before the delay expires, ``_cancel_background_tasks`` cancels this
|
|
486
|
+
task WITHOUT resetting the counter, so the next reconnect attempt
|
|
487
|
+
keeps the exponential backoff growing.
|
|
488
|
+
"""
|
|
489
|
+
await asyncio.sleep(self._stable_connection_time)
|
|
490
|
+
if self._state == ConnectionState.CONNECTED:
|
|
491
|
+
log.debug(
|
|
492
|
+
"Stable connection reached after %.1fs, resetting reconnect counter (was %d)",
|
|
493
|
+
self._stable_connection_time,
|
|
494
|
+
self._reconnect_count,
|
|
495
|
+
)
|
|
496
|
+
self._reconnect_count = 0
|
|
497
|
+
|
|
432
498
|
def _parse_message(self, message: str | bytes) -> Any:
|
|
433
499
|
"""Parse incoming message, attempting JSON decode for strings."""
|
|
434
500
|
if isinstance(message, str):
|
|
@@ -524,15 +590,27 @@ class WebSocketManager:
|
|
|
524
590
|
if was_connected:
|
|
525
591
|
self._stats.record_disconnect(proactive=reason.is_proactive)
|
|
526
592
|
|
|
593
|
+
# Code 1013 "Try Again Later": server explicitly asks clients to back off.
|
|
594
|
+
# Force a pre-reconnect delay regardless of the retry strategy.
|
|
595
|
+
if code == WS_CODE_TRY_AGAIN_LATER:
|
|
596
|
+
self._force_backoff_delay = self._server_unavailable_delay
|
|
597
|
+
log.warning(
|
|
598
|
+
"Server unavailable (1013), waiting %.0fs before reconnect",
|
|
599
|
+
self._server_unavailable_delay,
|
|
600
|
+
)
|
|
601
|
+
|
|
527
602
|
# Cancel background tasks
|
|
528
603
|
await self._cancel_background_tasks()
|
|
529
604
|
|
|
530
|
-
# Invoke on_disconnect callback
|
|
605
|
+
# Invoke on_disconnect callback (per-event)
|
|
531
606
|
if self._on_disconnect is not None:
|
|
532
607
|
result = self._on_disconnect(reason)
|
|
533
608
|
if asyncio.iscoroutine(result):
|
|
534
609
|
await result
|
|
535
610
|
|
|
611
|
+
# Throttled alert callback (aggregated count since last alert)
|
|
612
|
+
await self._maybe_emit_disconnect_alert(reason)
|
|
613
|
+
|
|
536
614
|
log.info("WebSocket disconnected: %s (code=%d)", reason.name, code)
|
|
537
615
|
|
|
538
616
|
# Handle reconnection
|
|
@@ -552,6 +630,33 @@ class WebSocketManager:
|
|
|
552
630
|
self._state = ConnectionState.DISCONNECTED
|
|
553
631
|
self._disconnected_event.set()
|
|
554
632
|
|
|
633
|
+
async def _maybe_emit_disconnect_alert(self, reason: DisconnectReason) -> None:
|
|
634
|
+
"""Emit ``on_disconnect_alert`` if the throttle window has elapsed.
|
|
635
|
+
|
|
636
|
+
The aggregated count is the number of disconnects observed since
|
|
637
|
+
the previous alert (including this one). After firing, the counter
|
|
638
|
+
is reset to 0 and the timestamp updated.
|
|
639
|
+
"""
|
|
640
|
+
if self._on_disconnect_alert is None:
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
self._disconnect_alert_count += 1
|
|
644
|
+
|
|
645
|
+
now = time.monotonic()
|
|
646
|
+
if now - self._last_disconnect_alert_at < self._disconnect_alert_interval:
|
|
647
|
+
return
|
|
648
|
+
|
|
649
|
+
count = self._disconnect_alert_count
|
|
650
|
+
self._disconnect_alert_count = 0
|
|
651
|
+
self._last_disconnect_alert_at = now
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
result = self._on_disconnect_alert(reason, count)
|
|
655
|
+
if asyncio.iscoroutine(result):
|
|
656
|
+
await result
|
|
657
|
+
except Exception:
|
|
658
|
+
log.exception("Error in on_disconnect_alert callback")
|
|
659
|
+
|
|
555
660
|
async def _reconnect_check_loop(self) -> None:
|
|
556
661
|
"""Background task to check should_reconnect callback."""
|
|
557
662
|
while self._state == ConnectionState.RECONNECTING and not self._shutdown_event.is_set():
|
|
@@ -582,11 +687,22 @@ class WebSocketManager:
|
|
|
582
687
|
log.warning("Error in should_reconnect callback: %s", e)
|
|
583
688
|
|
|
584
689
|
async def _attempt_reconnect(self) -> None:
|
|
585
|
-
"""Attempt to reconnect with retry logic.
|
|
690
|
+
"""Attempt to reconnect with retry logic.
|
|
691
|
+
|
|
692
|
+
Honors ``_force_backoff_delay`` first if set (e.g. after close
|
|
693
|
+
code 1013), then applies the strategy-based backoff.
|
|
694
|
+
"""
|
|
586
695
|
if self._shutdown_event.is_set():
|
|
587
696
|
log.debug("Shutdown requested, skipping reconnect attempt")
|
|
588
697
|
return
|
|
589
698
|
|
|
699
|
+
if self._force_backoff_delay is not None:
|
|
700
|
+
delay = self._force_backoff_delay
|
|
701
|
+
self._force_backoff_delay = None
|
|
702
|
+
await asyncio.sleep(delay)
|
|
703
|
+
if self._shutdown_event.is_set():
|
|
704
|
+
return
|
|
705
|
+
|
|
590
706
|
self._reconnect_count += 1
|
|
591
707
|
|
|
592
708
|
if self._reconnect_count > self._max_reconnect_attempts:
|
|
@@ -635,12 +751,18 @@ class WebSocketManager:
|
|
|
635
751
|
await asyncio.sleep(delay)
|
|
636
752
|
|
|
637
753
|
async def _cancel_background_tasks(self) -> None:
|
|
638
|
-
"""Cancel all background tasks.
|
|
754
|
+
"""Cancel all background tasks.
|
|
755
|
+
|
|
756
|
+
Note: cancelling ``_stable_connection_task`` does NOT reset
|
|
757
|
+
``_reconnect_count``. If the connection was torn down before the
|
|
758
|
+
stable window elapsed, the caller is treated as still flapping.
|
|
759
|
+
"""
|
|
639
760
|
tasks = [
|
|
640
761
|
self._receive_task,
|
|
641
762
|
self._disconnect_check_task,
|
|
642
763
|
self._reconnect_check_task,
|
|
643
764
|
self._ping_task,
|
|
765
|
+
self._stable_connection_task,
|
|
644
766
|
]
|
|
645
767
|
for task in tasks:
|
|
646
768
|
if task is not None and not task.done():
|
|
@@ -652,6 +774,7 @@ class WebSocketManager:
|
|
|
652
774
|
self._disconnect_check_task = None
|
|
653
775
|
self._reconnect_check_task = None
|
|
654
776
|
self._ping_task = None
|
|
777
|
+
self._stable_connection_task = None
|
|
655
778
|
|
|
656
779
|
async def _resubscribe(self) -> None:
|
|
657
780
|
"""Re-subscribe to all channels after reconnection."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kstlib
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Config-driven helpers for Python projects (dynamic config, secure secrets, preset logging, and more…)
|
|
5
5
|
Author-email: Michel TRUONG <michel.truong@gmail.com>
|
|
6
6
|
Maintainer-email: Michel TRUONG <michel.truong@gmail.com>
|
|
@@ -42,7 +42,7 @@ Requires-Dist: websockets<16,>=15.0
|
|
|
42
42
|
Requires-Dist: jinja2<4,>=3.1.5
|
|
43
43
|
Requires-Dist: humanize<5,>=4.11
|
|
44
44
|
Requires-Dist: httpx<1,>=0.28
|
|
45
|
-
Requires-Dist: authlib<2,>=1.6.
|
|
45
|
+
Requires-Dist: authlib<2,>=1.6.11
|
|
46
46
|
Requires-Dist: pendulum<4,>=3.0
|
|
47
47
|
Requires-Dist: cryptography>=46.0.7
|
|
48
48
|
Requires-Dist: requests>=2.33.0
|
|
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
|
|
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
|
|
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
|