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.
- localstack_cli/__init__.py +0 -0
- localstack_cli/cli/__init__.py +10 -0
- localstack_cli/cli/console.py +11 -0
- localstack_cli/cli/core_plugin.py +12 -0
- localstack_cli/cli/exceptions.py +19 -0
- localstack_cli/cli/localstack.py +951 -0
- localstack_cli/cli/lpm.py +138 -0
- localstack_cli/cli/main.py +22 -0
- localstack_cli/cli/plugin.py +39 -0
- localstack_cli/cli/plugins.py +134 -0
- localstack_cli/cli/profiles.py +65 -0
- localstack_cli/config.py +1689 -0
- localstack_cli/constants.py +165 -0
- localstack_cli/logging/__init__.py +0 -0
- localstack_cli/logging/format.py +194 -0
- localstack_cli/logging/setup.py +142 -0
- localstack_cli/packages/__init__.py +25 -0
- localstack_cli/packages/api.py +418 -0
- localstack_cli/packages/core.py +416 -0
- localstack_cli/pro/__init__.py +0 -0
- localstack_cli/pro/core/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/__init__.py +1 -0
- localstack_cli/pro/core/bootstrap/auth.py +213 -0
- localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
- localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
- localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
- localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
- localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
- localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
- localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
- localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
- localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
- localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
- localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
- localstack_cli/pro/core/cli/__init__.py +0 -0
- localstack_cli/pro/core/cli/auth.py +226 -0
- localstack_cli/pro/core/cli/aws.py +16 -0
- localstack_cli/pro/core/cli/cli.py +99 -0
- localstack_cli/pro/core/cli/click_utils.py +21 -0
- localstack_cli/pro/core/cli/cloud_pods.py +465 -0
- localstack_cli/pro/core/cli/diff_view.py +41 -0
- localstack_cli/pro/core/cli/ephemeral.py +199 -0
- localstack_cli/pro/core/cli/extensions.py +492 -0
- localstack_cli/pro/core/cli/iam.py +180 -0
- localstack_cli/pro/core/cli/license.py +90 -0
- localstack_cli/pro/core/cli/localstack.py +118 -0
- localstack_cli/pro/core/cli/replicator.py +378 -0
- localstack_cli/pro/core/cli/state.py +183 -0
- localstack_cli/pro/core/cli/tree_view.py +235 -0
- localstack_cli/pro/core/config.py +556 -0
- localstack_cli/pro/core/constants.py +54 -0
- localstack_cli/pro/core/plugins.py +169 -0
- localstack_cli/runtime/__init__.py +6 -0
- localstack_cli/runtime/exceptions.py +7 -0
- localstack_cli/runtime/hooks.py +73 -0
- localstack_cli/testing/__init__.py +1 -0
- localstack_cli/testing/config.py +4 -0
- localstack_cli/utils/__init__.py +0 -0
- localstack_cli/utils/analytics/__init__.py +12 -0
- localstack_cli/utils/analytics/cli.py +67 -0
- localstack_cli/utils/analytics/client.py +111 -0
- localstack_cli/utils/analytics/events.py +30 -0
- localstack_cli/utils/analytics/logger.py +48 -0
- localstack_cli/utils/analytics/metadata.py +250 -0
- localstack_cli/utils/analytics/publisher.py +160 -0
- localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
- localstack_cli/utils/archives.py +271 -0
- localstack_cli/utils/batching.py +258 -0
- localstack_cli/utils/bootstrap.py +1418 -0
- localstack_cli/utils/checksum.py +313 -0
- localstack_cli/utils/collections.py +554 -0
- localstack_cli/utils/common.py +229 -0
- localstack_cli/utils/container_networking.py +142 -0
- localstack_cli/utils/container_utils/__init__.py +0 -0
- localstack_cli/utils/container_utils/container_client.py +1585 -0
- localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
- localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
- localstack_cli/utils/crypto.py +294 -0
- localstack_cli/utils/docker_utils.py +272 -0
- localstack_cli/utils/files.py +327 -0
- localstack_cli/utils/functions.py +92 -0
- localstack_cli/utils/http.py +326 -0
- localstack_cli/utils/json.py +219 -0
- localstack_cli/utils/net.py +516 -0
- localstack_cli/utils/no_exit_argument_parser.py +19 -0
- localstack_cli/utils/numbers.py +49 -0
- localstack_cli/utils/objects.py +235 -0
- localstack_cli/utils/patch.py +260 -0
- localstack_cli/utils/platform.py +77 -0
- localstack_cli/utils/run.py +514 -0
- localstack_cli/utils/server/__init__.py +0 -0
- localstack_cli/utils/server/tcp_proxy.py +108 -0
- localstack_cli/utils/serving.py +187 -0
- localstack_cli/utils/ssl.py +71 -0
- localstack_cli/utils/strings.py +245 -0
- localstack_cli/utils/sync.py +267 -0
- localstack_cli/utils/threads.py +163 -0
- localstack_cli/utils/time.py +81 -0
- localstack_cli/utils/urls.py +21 -0
- localstack_cli/utils/venv.py +100 -0
- localstack_cli/utils/xml.py +41 -0
- localstack_cli/version.py +34 -0
- playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
- playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
- playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
- playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
- 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,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."""
|
|
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
|
+
)
|