playground-ls-cli 4.14.1.dev8__py3-none-any.whl

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.
Files changed (112) hide show
  1. localstack_cli/__init__.py +0 -0
  2. localstack_cli/cli/__init__.py +10 -0
  3. localstack_cli/cli/console.py +11 -0
  4. localstack_cli/cli/core_plugin.py +12 -0
  5. localstack_cli/cli/exceptions.py +19 -0
  6. localstack_cli/cli/localstack.py +951 -0
  7. localstack_cli/cli/lpm.py +138 -0
  8. localstack_cli/cli/main.py +22 -0
  9. localstack_cli/cli/plugin.py +39 -0
  10. localstack_cli/cli/plugins.py +134 -0
  11. localstack_cli/cli/profiles.py +65 -0
  12. localstack_cli/config.py +1689 -0
  13. localstack_cli/constants.py +165 -0
  14. localstack_cli/logging/__init__.py +0 -0
  15. localstack_cli/logging/format.py +194 -0
  16. localstack_cli/logging/setup.py +142 -0
  17. localstack_cli/packages/__init__.py +25 -0
  18. localstack_cli/packages/api.py +418 -0
  19. localstack_cli/packages/core.py +416 -0
  20. localstack_cli/pro/__init__.py +0 -0
  21. localstack_cli/pro/core/__init__.py +0 -0
  22. localstack_cli/pro/core/bootstrap/__init__.py +1 -0
  23. localstack_cli/pro/core/bootstrap/auth.py +213 -0
  24. localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
  25. localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
  26. localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
  27. localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
  28. localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
  29. localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
  30. localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
  31. localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
  32. localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
  33. localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
  34. localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
  35. localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
  36. localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
  37. localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
  38. localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
  39. localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
  40. localstack_cli/pro/core/cli/__init__.py +0 -0
  41. localstack_cli/pro/core/cli/auth.py +226 -0
  42. localstack_cli/pro/core/cli/aws.py +16 -0
  43. localstack_cli/pro/core/cli/cli.py +99 -0
  44. localstack_cli/pro/core/cli/click_utils.py +21 -0
  45. localstack_cli/pro/core/cli/cloud_pods.py +465 -0
  46. localstack_cli/pro/core/cli/diff_view.py +41 -0
  47. localstack_cli/pro/core/cli/ephemeral.py +199 -0
  48. localstack_cli/pro/core/cli/extensions.py +492 -0
  49. localstack_cli/pro/core/cli/iam.py +180 -0
  50. localstack_cli/pro/core/cli/license.py +90 -0
  51. localstack_cli/pro/core/cli/localstack.py +118 -0
  52. localstack_cli/pro/core/cli/replicator.py +378 -0
  53. localstack_cli/pro/core/cli/state.py +183 -0
  54. localstack_cli/pro/core/cli/tree_view.py +235 -0
  55. localstack_cli/pro/core/config.py +556 -0
  56. localstack_cli/pro/core/constants.py +54 -0
  57. localstack_cli/pro/core/plugins.py +169 -0
  58. localstack_cli/runtime/__init__.py +6 -0
  59. localstack_cli/runtime/exceptions.py +7 -0
  60. localstack_cli/runtime/hooks.py +73 -0
  61. localstack_cli/testing/__init__.py +1 -0
  62. localstack_cli/testing/config.py +4 -0
  63. localstack_cli/utils/__init__.py +0 -0
  64. localstack_cli/utils/analytics/__init__.py +12 -0
  65. localstack_cli/utils/analytics/cli.py +67 -0
  66. localstack_cli/utils/analytics/client.py +111 -0
  67. localstack_cli/utils/analytics/events.py +30 -0
  68. localstack_cli/utils/analytics/logger.py +48 -0
  69. localstack_cli/utils/analytics/metadata.py +250 -0
  70. localstack_cli/utils/analytics/publisher.py +160 -0
  71. localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
  72. localstack_cli/utils/archives.py +271 -0
  73. localstack_cli/utils/batching.py +258 -0
  74. localstack_cli/utils/bootstrap.py +1418 -0
  75. localstack_cli/utils/checksum.py +313 -0
  76. localstack_cli/utils/collections.py +554 -0
  77. localstack_cli/utils/common.py +229 -0
  78. localstack_cli/utils/container_networking.py +142 -0
  79. localstack_cli/utils/container_utils/__init__.py +0 -0
  80. localstack_cli/utils/container_utils/container_client.py +1585 -0
  81. localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
  82. localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
  83. localstack_cli/utils/crypto.py +294 -0
  84. localstack_cli/utils/docker_utils.py +272 -0
  85. localstack_cli/utils/files.py +327 -0
  86. localstack_cli/utils/functions.py +92 -0
  87. localstack_cli/utils/http.py +326 -0
  88. localstack_cli/utils/json.py +219 -0
  89. localstack_cli/utils/net.py +516 -0
  90. localstack_cli/utils/no_exit_argument_parser.py +19 -0
  91. localstack_cli/utils/numbers.py +49 -0
  92. localstack_cli/utils/objects.py +235 -0
  93. localstack_cli/utils/patch.py +260 -0
  94. localstack_cli/utils/platform.py +77 -0
  95. localstack_cli/utils/run.py +514 -0
  96. localstack_cli/utils/server/__init__.py +0 -0
  97. localstack_cli/utils/server/tcp_proxy.py +108 -0
  98. localstack_cli/utils/serving.py +187 -0
  99. localstack_cli/utils/ssl.py +71 -0
  100. localstack_cli/utils/strings.py +245 -0
  101. localstack_cli/utils/sync.py +267 -0
  102. localstack_cli/utils/threads.py +163 -0
  103. localstack_cli/utils/time.py +81 -0
  104. localstack_cli/utils/urls.py +21 -0
  105. localstack_cli/utils/venv.py +100 -0
  106. localstack_cli/utils/xml.py +41 -0
  107. localstack_cli/version.py +34 -0
  108. playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
  109. playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
  110. playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
  111. playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
  112. playground_ls_cli-4.14.1.dev8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,169 @@
1
+ """
2
+ Pro plugin hooks for the LocalStack CLI.
3
+
4
+ These hooks are executed on the host machine when running CLI commands like `localstack start`.
5
+ They handle license activation, container configuration, and extension developer mode.
6
+
7
+ Note: This file was extracted from localstack-pro-core/localstack/pro/core/plugins.py
8
+ and rewritten to contain only CLI-relevant functionality.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import os
15
+
16
+ import requests
17
+ from localstack_cli import config as localstack_config
18
+ from localstack_cli.config import HostAndPort
19
+ from localstack_cli.constants import API_ENDPOINT
20
+ from localstack_cli.pro.core import config as pro_config
21
+ from localstack_cli.pro.core.bootstrap import licensingv2
22
+ from localstack_cli.runtime import hooks
23
+ from localstack_cli.runtime.exceptions import LocalstackExit
24
+ from localstack_cli.utils.bootstrap import Container
25
+
26
+ LOG = logging.getLogger(__name__)
27
+
28
+
29
+ def modify_gateway_listen_config(cfg):
30
+ """
31
+ Modifies the localstack config to additionally listen to port 443.
32
+ Needs to be called before any edge URLs are resolved using the config.
33
+ """
34
+ if os.getenv("GATEWAY_LISTEN") is None:
35
+ host = "0.0.0.0" if localstack_config.in_docker() else "127.0.0.1"
36
+ cfg.GATEWAY_LISTEN.append(HostAndPort(host=host, port=443))
37
+
38
+
39
+ @hooks.prepare_host(priority=200)
40
+ def patch_community_pro_detection():
41
+ """This is currently needed to make localstack core aware of the `localstack auth set-token`
42
+ functionality, where we set the key into the ``~/.localstack/auth.json`` file that community does not
43
+ yet know about. ``is_api_key_configured`` is used in the LocalStack CLI to determine whether to start
44
+ the localstack or localstack-pro container image."""
45
+ from localstack_cli.utils import bootstrap
46
+
47
+ bootstrap.is_auth_token_configured = pro_config.is_auth_token_configured
48
+
49
+
50
+ ## ---------------------------------------------------------------------------
51
+ ## Grace period helpers (temporary, to be removed once grace period expires)
52
+ ## ---------------------------------------------------------------------------
53
+
54
+ GRACE_PERIOD_ENDPOINT_PATH = "/license/grace-period-check"
55
+
56
+ LICENSE_ERROR_MESSAGE = """\
57
+ ===============================================
58
+ LocalStack requires an account to run.
59
+
60
+ ==> Have an account? Learn how to set LOCALSTACK_AUTH_TOKEN: https://app.localstack.cloud/settings/auth-tokens
61
+ ==> Need an account? Get started: https://www.localstack.cloud/pricing
62
+ ==> Want more time? Snooze until April 6, 2026 by setting LOCALSTACK_ACKNOWLEDGE_ACCOUNT_REQUIREMENT=1
63
+ """
64
+
65
+ GRACE_PERIOD_EXPIRED_MESSAGE = """\
66
+ ===============================================
67
+ LocalStack requires an account to run.
68
+
69
+ ==> Have an account? Learn how to set LOCALSTACK_AUTH_TOKEN: https://app.localstack.cloud/settings/auth-tokens
70
+ ==> Need an account? Get started: https://www.localstack.cloud/pricing
71
+ """
72
+
73
+
74
+ def _check_grace_period_active(ack: bool) -> bool:
75
+ """Call the platform grace period endpoint.
76
+
77
+ Returns True if the grace period is active (200 response).
78
+ Returns False for any other response (including 404 when the endpoint is removed).
79
+ """
80
+ from localstack_cli.pro.core.bootstrap.licensingv2 import get_system_information_summary
81
+ from localstack_cli.pro.core.constants import VERSION
82
+ from localstack_cli.utils.analytics.metadata import get_client_metadata
83
+ from localstack_cli.utils.http import get_proxies
84
+
85
+ metadata = get_client_metadata()
86
+ payload = {
87
+ "machine": {
88
+ "id": metadata.machine_id,
89
+ "ci": metadata.is_ci,
90
+ "system": get_system_information_summary(),
91
+ },
92
+ "version": VERSION,
93
+ "requesting_grace": ack,
94
+ }
95
+
96
+ url = f"{API_ENDPOINT}{GRACE_PERIOD_ENDPOINT_PATH}"
97
+ proxies = get_proxies()
98
+
99
+ try:
100
+ response = requests.post(
101
+ url,
102
+ json=payload,
103
+ verify=not localstack_config.is_env_true("SSL_NO_VERIFY"),
104
+ proxies=proxies,
105
+ timeout=10,
106
+ )
107
+ return response.ok
108
+ except requests.exceptions.RequestException:
109
+ LOG.debug("Failed to reach grace period endpoint at %s", url)
110
+ return False
111
+
112
+
113
+ ## ---------------------------------------------------------------------------
114
+
115
+
116
+ @hooks.prepare_host(priority=100, should_load=pro_config.ACTIVATE_PRO)
117
+ def activate_pro_key_on_host():
118
+ """Activate license on host (needed for DNS forward and EC2 daemon)."""
119
+ try:
120
+ licensingv2.get_licensed_environment().activate()
121
+ except licensingv2.LicensingError as e:
122
+ # license activation was unsuccessful (this can also be because no auth token was set)
123
+ # defensively set pro to False so that we don't load pro plugins
124
+ pro_config.ACTIVATE_PRO = False
125
+
126
+ # check also with LOCALSTACK_ prefix here because we don't have the handling from the docker entrypoint on the host
127
+ ack = localstack_config.is_env_true(
128
+ "LOCALSTACK_ACKNOWLEDGE_ACCOUNT_REQUIREMENT") or localstack_config.is_env_true(
129
+ "ACKNOWLEDGE_ACCOUNT_REQUIREMENT")
130
+ # Note: this can also fail because they've got their connection set up wrong.
131
+ active = _check_grace_period_active(ack)
132
+ if not ack and active:
133
+ # Grace period is active but unused. Prompt user to set up account or snooze.
134
+ raise LocalstackExit(reason=LICENSE_ERROR_MESSAGE, code=55)
135
+ if ack and active:
136
+ # Grace period is active, start in community mode (no pro features).
137
+ LOG.info("Grace period active: starting LocalStack in community mode")
138
+ return
139
+ if ack and not active:
140
+ # Grace period expired or endpoint unreachable
141
+ raise LocalstackExit(reason=GRACE_PERIOD_EXPIRED_MESSAGE, code=55)
142
+ raise LocalstackExit(reason=e.get_user_friendly(), code=55)
143
+
144
+
145
+ @hooks.configure_localstack_container(priority=10, should_load=pro_config.ACTIVATE_PRO)
146
+ def configure_pro_container(container: Container):
147
+ """Configure the LocalStack container for pro features."""
148
+ modify_gateway_listen_config(localstack_config)
149
+ container.configure(licensingv2.configure_container_licensing)
150
+
151
+
152
+ @hooks.prepare_host(should_load=pro_config.ACTIVATE_PRO and pro_config.EXTENSION_DEV_MODE)
153
+ def configure_extensions_dev_host():
154
+ """Load extension directories from ~/.localstack/extensions-dev.json."""
155
+ from localstack_cli.pro.core.bootstrap.extensions.bootstrap import run_on_configure_host_hook
156
+
157
+ run_on_configure_host_hook()
158
+
159
+
160
+ @hooks.configure_localstack_container(
161
+ should_load=pro_config.ACTIVATE_PRO and pro_config.EXTENSION_DEV_MODE
162
+ )
163
+ def configure_extensions_dev_container(container: Container):
164
+ """Configure container for extension developer mode."""
165
+ from localstack_cli.pro.core.bootstrap.extensions.bootstrap import (
166
+ run_on_configure_localstack_container_hook,
167
+ )
168
+
169
+ run_on_configure_localstack_container_hook(container)
@@ -0,0 +1,6 @@
1
+ """Runtime utilities for the LocalStack CLI."""
2
+
3
+ from localstack_cli.runtime import hooks # noqa: F401
4
+
5
+ # get_current_runtime is not available in the standalone CLI - only in the full LocalStack runtime
6
+ get_current_runtime = None
@@ -0,0 +1,7 @@
1
+ class LocalstackExit(Exception):
2
+ """Raised to gracefully exit LocalStack."""
3
+
4
+ def __init__(self, reason: str = None, code: int = 1):
5
+ self.reason = reason
6
+ self.code = code
7
+ super().__init__(reason)
@@ -0,0 +1,73 @@
1
+ import functools
2
+
3
+ from plux import PluginManager, plugin
4
+
5
+ # plugin namespace constants
6
+ HOOKS_CONFIGURE_LOCALSTACK_CONTAINER = "localstack_cli.hooks.configure_localstack_container"
7
+ HOOKS_PREPARE_HOST = "localstack_cli.hooks.prepare_host"
8
+
9
+
10
+ def hook(namespace: str, priority: int = 0, **kwargs):
11
+ """
12
+ Decorator for creating functional plugins that have a hook_priority attribute. Hooks with a higher priority value
13
+ will be executed earlier.
14
+ """
15
+
16
+ def wrapper(fn):
17
+ fn.hook_priority = priority
18
+ return plugin(namespace=namespace, **kwargs)(fn)
19
+
20
+ return wrapper
21
+
22
+
23
+ def hook_spec(namespace: str):
24
+ """
25
+ Creates a new hook decorator bound to a namespace.
26
+
27
+ on_infra_start = hook_spec("localstack.hooks.on_infra_start")
28
+
29
+ @on_infra_start()
30
+ def foo():
31
+ pass
32
+
33
+ # run all hooks in order
34
+ on_infra_start.run()
35
+ """
36
+ fn = functools.partial(hook, namespace=namespace)
37
+ # attach hook manager and run method to decorator for convenience calls
38
+ fn.manager = HookManager(namespace)
39
+ fn.run = fn.manager.run_in_order
40
+ return fn
41
+
42
+
43
+ class HookManager(PluginManager):
44
+ def load_all_sorted(self, propagate_exceptions=False):
45
+ """
46
+ Loads all hook plugins and sorts them by their hook_priority attribute.
47
+ """
48
+ plugins = self.load_all(propagate_exceptions)
49
+ # the hook_priority attribute is part of the function wrapped in the FunctionPlugin
50
+ plugins.sort(
51
+ key=lambda _fn_plugin: getattr(_fn_plugin.fn, "hook_priority", 0), reverse=True
52
+ )
53
+ return plugins
54
+
55
+ def run_in_order(self, *args, **kwargs):
56
+ """
57
+ Loads and runs all plugins in order them with the given arguments.
58
+ """
59
+ for fn_plugin in self.load_all_sorted():
60
+ fn_plugin(*args, **kwargs)
61
+
62
+ def __str__(self):
63
+ return f"HookManager({self.namespace})"
64
+
65
+ def __repr__(self):
66
+ return self.__str__()
67
+
68
+
69
+ configure_localstack_container = hook_spec(HOOKS_CONFIGURE_LOCALSTACK_CONTAINER)
70
+ """Hooks to configure the LocalStack container before it starts. Executed on the host when invoking the CLI."""
71
+
72
+ prepare_host = hook_spec(HOOKS_PREPARE_HOST)
73
+ """Hooks to prepare the host that's starting LocalStack. Executed on the host when invoking the CLI."""
@@ -0,0 +1 @@
1
+ """Testing utilities for the LocalStack CLI."""
@@ -0,0 +1,4 @@
1
+ """Testing configuration constants."""
2
+
3
+ # Secondary test account ID for multi-account testing scenarios
4
+ SECONDARY_TEST_AWS_ACCOUNT_ID = "886468871268"
File without changes
@@ -0,0 +1,12 @@
1
+ from .logger import EventLogger
2
+ from .metadata import get_session_id
3
+ from .publisher import GlobalAnalyticsBus
4
+
5
+ name = "analytics"
6
+
7
+
8
+ def _create_global_analytics_bus():
9
+ return GlobalAnalyticsBus()
10
+
11
+
12
+ log = EventLogger(handler=_create_global_analytics_bus(), session_id=get_session_id())
@@ -0,0 +1,67 @@
1
+ import datetime
2
+ import functools
3
+ from multiprocessing import Process
4
+
5
+ import click
6
+
7
+ from localstack_cli import config
8
+
9
+ from .client import AnalyticsClient
10
+ from .events import Event, EventMetadata
11
+ from .metadata import get_session_id
12
+ from .publisher import AnalyticsClientPublisher
13
+
14
+ ANALYTICS_API_RESPONSE_TIMEOUT_SECS = 0.5
15
+
16
+
17
+ def _publish_cmd_as_analytics_event(command_name: str, params: list[str]):
18
+ event = Event(
19
+ name="cli_cmd",
20
+ payload={"cmd": command_name, "params": params},
21
+ metadata=EventMetadata(
22
+ session_id=get_session_id(),
23
+ client_time=str(datetime.datetime.now()), # TODO: consider using utcnow()
24
+ ),
25
+ )
26
+ publisher = AnalyticsClientPublisher(AnalyticsClient())
27
+ publisher.publish([event])
28
+
29
+
30
+ def _get_parent_commands(ctx: click.Context) -> list[str]:
31
+ parent_commands = []
32
+ parent = ctx.parent
33
+ while parent is not None:
34
+ parent_commands.insert(0, parent.command.name)
35
+ parent = parent.parent
36
+ return parent_commands
37
+
38
+
39
+ def publish_invocation(fn):
40
+ """
41
+ Decorator for capturing CLI commands from Click and publishing them to the backend as analytics events.
42
+ This decorator should only be used on outermost subcommands, e.g. "localstack status docker" not "localstack status"
43
+ otherwise it may publish multiple events for a single invocation.
44
+ If DISABLE_EVENTS is set then nothing is collected.
45
+ For performance reasons, the API call to the backend runs on a separate process and is killed if it takes longer
46
+ than ANALYTICS_API_RESPONSE_TIMEOUT_SECS.
47
+ The emitted event contains the invoked command, plus any parameter names if their associated values are truthy (but
48
+ not the values themselves).
49
+ """
50
+
51
+ @functools.wraps(fn)
52
+ def publisher_wrapper(*args, **kwargs):
53
+ if config.DISABLE_EVENTS:
54
+ return fn(*args, **kwargs)
55
+
56
+ ctx = click.get_current_context()
57
+ full_command = " ".join(_get_parent_commands(ctx) + [ctx.command.name])
58
+ publish_cmd_process = Process(
59
+ target=_publish_cmd_as_analytics_event,
60
+ args=(full_command, [k for k, v in ctx.params.items() if v]),
61
+ )
62
+ publish_cmd_process.start()
63
+ publish_cmd_process.join(ANALYTICS_API_RESPONSE_TIMEOUT_SECS)
64
+ publish_cmd_process.terminate()
65
+ return fn(*args, **kwargs)
66
+
67
+ return publisher_wrapper
@@ -0,0 +1,111 @@
1
+ """
2
+ Client for the analytics backend.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ import requests
9
+
10
+ from localstack_cli import config, constants
11
+ from localstack_cli.utils.http import get_proxies
12
+ from localstack_cli.utils.time import now
13
+
14
+ from .events import Event, EventMetadata
15
+ from .metadata import ClientMetadata, get_session_id
16
+
17
+ LOG = logging.getLogger(__name__)
18
+
19
+
20
+ class SessionResponse:
21
+ response: dict[str, Any]
22
+ status: int
23
+
24
+ def __init__(self, response: dict[str, Any], status: int = 200):
25
+ self.response = response
26
+ self.status = status
27
+
28
+ def track_events(self) -> bool:
29
+ return self.response.get("track_events")
30
+
31
+ def __repr__(self):
32
+ return f"SessionResponse({self.status},{self.response!r})"
33
+
34
+
35
+ class AnalyticsClient:
36
+ api: str
37
+
38
+ def __init__(self, api=None):
39
+ self.api = (api or constants.ANALYTICS_API).rstrip("/")
40
+ self.debug = config.DEBUG_ANALYTICS
41
+
42
+ self.endpoint_session = self.api + "/session"
43
+ self.endpoint_events = self.api + "/events"
44
+
45
+ self.localstack_session_id = get_session_id()
46
+ self.session = requests.Session()
47
+
48
+ def close(self):
49
+ self.session.close()
50
+
51
+ def start_session(self, metadata: ClientMetadata) -> SessionResponse:
52
+ # FIXME: re-using Event as request object this way is kind of a hack
53
+ request = Event(
54
+ "session", EventMetadata(self.localstack_session_id, str(now())), payload=metadata
55
+ )
56
+
57
+ response = self.session.post(
58
+ self.endpoint_session,
59
+ headers=self._create_headers(),
60
+ json=request.asdict(),
61
+ proxies=get_proxies(),
62
+ )
63
+
64
+ # 403 errors may indicate that track_events=False
65
+ if response.ok or response.status_code == 403:
66
+ return SessionResponse(response.json(), status=response.status_code)
67
+
68
+ raise ValueError(
69
+ f"error during session initiation with analytics backend. code: {response.status_code}"
70
+ )
71
+
72
+ # TODO: naming seems confusing since this doesn't actually append, but directly sends all passed events via HTTP
73
+ def append_events(self, events: list[Event]):
74
+ # TODO: add compression to append_events
75
+ # it would maybe be useful to compress analytics data, but it's unclear how that will
76
+ # affect performance and what the benefit is. need to measure first.
77
+
78
+ endpoint = self.endpoint_events
79
+
80
+ if not events:
81
+ return
82
+
83
+ docs = []
84
+ for event in events:
85
+ try:
86
+ docs.append(event.asdict())
87
+ except Exception:
88
+ if self.debug:
89
+ LOG.exception("error while recording event %s", event)
90
+
91
+ headers = self._create_headers()
92
+
93
+ if self.debug:
94
+ LOG.debug("posting to %s events %s", endpoint, docs)
95
+
96
+ # FIXME: fault tolerance/timeouts
97
+ response = self.session.post(
98
+ endpoint, json={"events": docs}, headers=headers, proxies=get_proxies()
99
+ )
100
+
101
+ if self.debug:
102
+ LOG.debug("response from %s was: %s %s", endpoint, response.status_code, response.text)
103
+
104
+ # TODO: Add response type to analytics client
105
+ return response
106
+
107
+ def _create_headers(self) -> dict[str, str]:
108
+ return {
109
+ "User-Agent": "localstack/" + constants.VERSION,
110
+ "Localstack-Session-ID": self.localstack_session_id,
111
+ }
@@ -0,0 +1,30 @@
1
+ import abc
2
+ import dataclasses
3
+ from typing import Any
4
+
5
+ EventPayload = dict[str, Any] | Any # FIXME: better typing
6
+
7
+
8
+ @dataclasses.dataclass
9
+ class EventMetadata:
10
+ session_id: str
11
+ client_time: str
12
+
13
+
14
+ @dataclasses.dataclass
15
+ class Event:
16
+ name: str
17
+ metadata: EventMetadata = None
18
+ payload: EventPayload = None
19
+
20
+ def asdict(self):
21
+ return dataclasses.asdict(self)
22
+
23
+
24
+ class EventHandler(abc.ABC):
25
+ """
26
+ Event handlers dispatch events to specific destinations.
27
+ """
28
+
29
+ def handle(self, event: Event):
30
+ raise NotImplementedError
@@ -0,0 +1,48 @@
1
+ import datetime
2
+ import hashlib
3
+
4
+ from localstack_cli.utils.strings import to_bytes
5
+
6
+ from .events import Event, EventHandler, EventMetadata, EventPayload
7
+ from .metadata import get_session_id
8
+
9
+
10
+ def get_hash(value) -> str:
11
+ max_length = 10
12
+ digest = hashlib.sha1()
13
+ digest.update(to_bytes(str(value)))
14
+ result = digest.hexdigest()
15
+ return result[:max_length]
16
+
17
+
18
+ class EventLogger:
19
+ """
20
+ High-level interface over analytics event abstraction. Expose specific event types as
21
+ concrete functions to call in the code.
22
+ """
23
+
24
+ def __init__(self, handler: EventHandler, session_id: str = None):
25
+ self.handler = handler
26
+ self.session_id = session_id or get_session_id()
27
+
28
+ @staticmethod
29
+ def hash(value):
30
+ return get_hash(value)
31
+
32
+ def event(self, event: str, payload: EventPayload = None, **kwargs):
33
+ if kwargs:
34
+ if payload is None:
35
+ payload = kwargs
36
+ else:
37
+ raise ValueError("either use payload or set kwargs, not both")
38
+
39
+ self._log(event, payload=payload)
40
+
41
+ def _log(self, event: str, payload: EventPayload = None):
42
+ self.handler.handle(Event(name=event, metadata=self._metadata(), payload=payload))
43
+
44
+ def _metadata(self) -> EventMetadata:
45
+ return EventMetadata(
46
+ session_id=self.session_id,
47
+ client_time=str(datetime.datetime.now()),
48
+ )