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,250 @@
1
+ import dataclasses
2
+ import logging
3
+ import os
4
+ import platform
5
+
6
+ from localstack_cli import config
7
+ from localstack_cli.constants import VERSION
8
+ from localstack_cli.runtime import get_current_runtime, hooks
9
+ from localstack_cli.utils.bootstrap import Container
10
+ from localstack_cli.utils.files import rm_rf
11
+ from localstack_cli.utils.functions import call_safe
12
+ from localstack_cli.utils.json import FileMappedDocument
13
+ from localstack_cli.utils.objects import singleton_factory
14
+ from localstack_cli.utils.strings import long_uid, md5
15
+
16
+ LOG = logging.getLogger(__name__)
17
+
18
+ _PHYSICAL_ID_SALT = "ls"
19
+
20
+
21
+ @dataclasses.dataclass
22
+ class ClientMetadata:
23
+ session_id: str
24
+ machine_id: str
25
+ api_key: str
26
+ system: str
27
+ version: str
28
+ is_ci: bool
29
+ is_docker: bool
30
+ is_testing: bool
31
+ product: str
32
+ edition: str
33
+
34
+ def __repr__(self):
35
+ d = dataclasses.asdict(self)
36
+
37
+ # anonymize api_key
38
+ k = d.get("api_key")
39
+ if k:
40
+ k = "*" * len(k)
41
+ d["api_key"] = k
42
+
43
+ return f"ClientMetadata({d})"
44
+
45
+
46
+ def get_version_string() -> str:
47
+ gh = config.LOCALSTACK_BUILD_GIT_HASH
48
+ if gh:
49
+ return f"{VERSION}:{gh}"
50
+ else:
51
+ return VERSION
52
+
53
+
54
+ def read_client_metadata() -> ClientMetadata:
55
+ return ClientMetadata(
56
+ session_id=get_session_id(),
57
+ machine_id=get_machine_id(),
58
+ api_key=get_api_key_or_auth_token() or "", # api key should not be None
59
+ system=get_system(),
60
+ version=get_version_string(),
61
+ is_ci=os.getenv("CI") is not None,
62
+ is_docker=config.is_in_docker,
63
+ is_testing=config.is_local_test_mode(),
64
+ product=get_localstack_product(),
65
+ edition=os.getenv("LOCALSTACK_TELEMETRY_EDITION") or get_localstack_edition(),
66
+ )
67
+
68
+
69
+ @singleton_factory
70
+ def get_session_id() -> str:
71
+ """
72
+ Returns the unique ID for this LocalStack session.
73
+ :return: a UUID
74
+ """
75
+ return _generate_session_id()
76
+
77
+
78
+ @singleton_factory
79
+ def get_client_metadata() -> ClientMetadata:
80
+ metadata = read_client_metadata()
81
+
82
+ if config.DEBUG_ANALYTICS:
83
+ LOG.info("resolved client metadata: %s", metadata)
84
+
85
+ return metadata
86
+
87
+
88
+ @singleton_factory
89
+ def get_machine_id() -> str:
90
+ cache_path = os.path.join(config.dirs.cache, "machine.json")
91
+ try:
92
+ doc = FileMappedDocument(cache_path)
93
+ except Exception:
94
+ # it's possible that the file is somehow messed up, so we try to delete the file first and try again.
95
+ # if that fails, we return a generated ID.
96
+ call_safe(rm_rf, args=(cache_path,))
97
+
98
+ try:
99
+ doc = FileMappedDocument(cache_path)
100
+ except Exception:
101
+ return _generate_machine_id()
102
+
103
+ if "machine_id" not in doc:
104
+ # generate a machine id
105
+ doc["machine_id"] = _generate_machine_id()
106
+ # try to cache the machine ID
107
+ call_safe(doc.save)
108
+
109
+ return doc["machine_id"]
110
+
111
+
112
+ def get_localstack_edition() -> str:
113
+ # Generator expression to find the first hidden file ending with '-version'
114
+ version_file = next(
115
+ (
116
+ f
117
+ for f in os.listdir(config.dirs.static_libs)
118
+ if f.startswith(".") and f.endswith("-version")
119
+ ),
120
+ None,
121
+ )
122
+
123
+ # Return the base name of the version file, or unknown if no file is found
124
+ return version_file.removesuffix("-version").removeprefix(".") if version_file else "unknown"
125
+
126
+
127
+ def get_localstack_product() -> str:
128
+ """
129
+ Returns the telemetry product name from the env var, runtime, or "unknown".
130
+ """
131
+ runtime_product = None
132
+ if get_current_runtime is not None:
133
+ try:
134
+ runtime_product = get_current_runtime().components.name
135
+ except (ValueError, AttributeError):
136
+ pass
137
+
138
+ return os.getenv("LOCALSTACK_TELEMETRY_PRODUCT") or runtime_product or "unknown"
139
+
140
+
141
+ def is_license_activated() -> bool:
142
+ try:
143
+ from localstack_cli.pro.core import config # noqa
144
+ except ImportError:
145
+ return False
146
+
147
+ try:
148
+ from localstack_cli.pro.core.bootstrap import licensingv2
149
+
150
+ return licensingv2.get_licensed_environment().activated
151
+ except Exception:
152
+ LOG.error(
153
+ "Could not determine license activation status",
154
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
155
+ )
156
+ return False
157
+
158
+
159
+ def _generate_session_id() -> str:
160
+ return long_uid()
161
+
162
+
163
+ def _anonymize_physical_id(physical_id: str) -> str:
164
+ """
165
+ Returns 12 digits of the salted hash of the given physical ID.
166
+
167
+ :param physical_id: the physical id
168
+ :return: an anonymized 12 digit value representing the physical ID.
169
+ """
170
+ hashed = md5(_PHYSICAL_ID_SALT + physical_id)
171
+ return hashed[:12]
172
+
173
+
174
+ def _generate_machine_id() -> str:
175
+ try:
176
+ # try to get a robust ID from the docker socket (which will be the same from the host and the
177
+ # container)
178
+ from localstack_cli.utils.docker_utils import DOCKER_CLIENT
179
+
180
+ docker_id = DOCKER_CLIENT.get_system_id()
181
+ # some systems like podman don't return a stable ID, so we double-check that here
182
+ if docker_id == DOCKER_CLIENT.get_system_id():
183
+ return f"dkr_{_anonymize_physical_id(docker_id)}"
184
+ except Exception:
185
+ pass
186
+
187
+ if config.is_in_docker:
188
+ return f"gen_{long_uid()[:12]}"
189
+
190
+ # this can potentially be useful when generated on the host using the CLI and then mounted into the
191
+ # container via machine.json
192
+ try:
193
+ if os.path.exists("/etc/machine-id"):
194
+ with open("/etc/machine-id") as fd:
195
+ machine_id = str(fd.read()).strip()
196
+ if machine_id:
197
+ return f"sys_{_anonymize_physical_id(machine_id)}"
198
+ except Exception:
199
+ pass
200
+
201
+ # always fall back to a generated id
202
+ return f"gen_{long_uid()[:12]}"
203
+
204
+
205
+ def get_api_key_or_auth_token() -> str | None:
206
+ # TODO: this is duplicated code from ext, but should probably migrate that to localstack
207
+ auth_token = os.environ.get("LOCALSTACK_AUTH_TOKEN", "").strip("'\" ")
208
+ if auth_token:
209
+ return auth_token
210
+
211
+ api_key = os.environ.get("LOCALSTACK_API_KEY", "").strip("'\" ")
212
+ if api_key:
213
+ return api_key
214
+
215
+ return None
216
+
217
+
218
+ @singleton_factory
219
+ def get_system() -> str:
220
+ try:
221
+ # try to get the system from the docker socket
222
+ from localstack_cli.utils.docker_utils import DOCKER_CLIENT
223
+
224
+ system = DOCKER_CLIENT.get_system_info()
225
+ if system.get("OSType"):
226
+ return system.get("OSType").lower()
227
+ except Exception:
228
+ pass
229
+
230
+ if config.is_in_docker:
231
+ return "docker"
232
+
233
+ return platform.system().lower()
234
+
235
+
236
+ @hooks.prepare_host()
237
+ def publish_invocation_metadata():
238
+ """Lazy-init machine ID into cache on the host, which can then be used in the container."""
239
+ get_machine_id()
240
+
241
+
242
+ @hooks.configure_localstack_container()
243
+ def set_analytics_header(container: Container):
244
+ """Mount the machine file from the host's CLI cache directory into the container."""
245
+ from localstack_cli.utils.container_utils.container_client import BindMount
246
+
247
+ machine_file = os.path.join(config.dirs.cache, "machine.json")
248
+ if os.path.isfile(machine_file):
249
+ target = os.path.join(config.dirs.for_container().cache, "machine.json")
250
+ container.config.volumes.add(BindMount(machine_file, target, read_only=True))
@@ -0,0 +1,160 @@
1
+ import abc
2
+ import atexit
3
+ import logging
4
+ import threading
5
+
6
+ from localstack_cli import config
7
+ from localstack_cli.utils.batching import AsyncBatcher
8
+ from localstack_cli.utils.threads import FuncThread, start_thread, start_worker_thread
9
+
10
+ from .client import AnalyticsClient
11
+ from .events import Event, EventHandler
12
+ from .metadata import get_client_metadata
13
+
14
+ LOG = logging.getLogger(__name__)
15
+
16
+
17
+ class Publisher(abc.ABC):
18
+ """
19
+ A publisher takes a batch of events and publishes them to a destination.
20
+ """
21
+
22
+ def publish(self, events: list[Event]):
23
+ raise NotImplementedError
24
+
25
+ def close(self):
26
+ pass
27
+
28
+
29
+ class AnalyticsClientPublisher(Publisher):
30
+ client: AnalyticsClient
31
+
32
+ def __init__(self, client: AnalyticsClient = None) -> None:
33
+ super().__init__()
34
+ self.client = client or AnalyticsClient()
35
+
36
+ def publish(self, events: list[Event]):
37
+ self.client.append_events(events)
38
+
39
+ def close(self):
40
+ self.client.close()
41
+
42
+
43
+ class Printer(Publisher):
44
+ """
45
+ Publisher that prints serialized events to stdout.
46
+ """
47
+
48
+ def publish(self, events: list[Event]):
49
+ for event in events:
50
+ print(event.asdict())
51
+
52
+
53
+ class GlobalAnalyticsBus(EventHandler):
54
+ _batcher: AsyncBatcher[Event]
55
+ _client: AnalyticsClient
56
+ _worker_thread: FuncThread | None
57
+
58
+ def __init__(self, client: AnalyticsClient = None, flush_size=20, flush_interval=10) -> None:
59
+ self._client = client or AnalyticsClient()
60
+ self._publisher = AnalyticsClientPublisher(self._client)
61
+ self._batcher = AsyncBatcher(
62
+ self._handle_batch,
63
+ max_batch_size=flush_size,
64
+ max_flush_interval=flush_interval,
65
+ )
66
+
67
+ self._started = False
68
+ self._startup_mutex = threading.Lock()
69
+ self._worker_thread = None
70
+
71
+ self.force_tracking = False # allow class to ignore all other tracking config
72
+ self.tracking_disabled = False # disables tracking if global config would otherwise track
73
+
74
+ def _handle_batch(self, batch: list[Event]):
75
+ """Method that satisfies the BatchHandler[Event] protocol and is passed to AsyncBatcher."""
76
+ try:
77
+ self._publisher.publish(batch)
78
+ except Exception:
79
+ # currently we're just dropping events if something goes wrong during publishing
80
+ if config.DEBUG_ANALYTICS:
81
+ LOG.exception("error while publishing analytics events")
82
+
83
+ @property
84
+ def is_tracking_disabled(self):
85
+ if self.force_tracking:
86
+ return False
87
+
88
+ # don't track if event tracking is disabled globally
89
+ if config.DISABLE_EVENTS:
90
+ return True
91
+ # don't track for internal test runs (like integration tests)
92
+ if config.is_local_test_mode():
93
+ return True
94
+ if self.tracking_disabled:
95
+ return True
96
+
97
+ return False
98
+
99
+ def handle(self, event: Event):
100
+ """
101
+ Publish an event to the global analytics event publisher.
102
+ """
103
+ if self.is_tracking_disabled:
104
+ if config.DEBUG_ANALYTICS:
105
+ LOG.debug("tracking disabled, skipping event %s", event)
106
+ return
107
+
108
+ if not self._started:
109
+ # we make sure the batching worker is started
110
+ self._start()
111
+
112
+ self._batcher.add(event)
113
+
114
+ def _start(self):
115
+ with self._startup_mutex:
116
+ if self._started:
117
+ return
118
+ self._started = True
119
+
120
+ # startup has to run async, otherwise first call to handle() could block a long time.
121
+ start_worker_thread(self._do_start_retry)
122
+
123
+ def _do_start_retry(self, *_):
124
+ # TODO: actually retry
125
+ try:
126
+ if config.DEBUG_ANALYTICS:
127
+ LOG.debug("trying to register session with analytics backend")
128
+ response = self._client.start_session(get_client_metadata())
129
+ if config.DEBUG_ANALYTICS:
130
+ LOG.debug("session endpoint returned: %s", response)
131
+
132
+ if not response.track_events():
133
+ if config.DEBUG_ANALYTICS:
134
+ LOG.debug("gracefully disabling analytics tracking")
135
+ self.tracking_disabled = True
136
+
137
+ except Exception:
138
+ self.tracking_disabled = True
139
+ if config.DEBUG_ANALYTICS:
140
+ LOG.exception("error while registering session. disabling tracking")
141
+ return
142
+
143
+ self._worker_thread = start_thread(self._run, name="global-analytics-bus")
144
+
145
+ # given the "Global" nature of this class, we register a global atexit hook to make sure all events are flushed
146
+ # when localstack shuts down.
147
+ def _do_close():
148
+ self.close_sync(timeout=2)
149
+
150
+ atexit.register(_do_close)
151
+
152
+ def _run(self, *_):
153
+ # main control loop, simply runs the batcher
154
+ self._batcher.run()
155
+
156
+ def close_sync(self, timeout=None):
157
+ self._batcher.close()
158
+
159
+ if self._worker_thread:
160
+ self._worker_thread.join(timeout=timeout)
@@ -0,0 +1,133 @@
1
+ import datetime
2
+ import logging
3
+ import threading
4
+ from collections import Counter
5
+ from typing import NamedTuple
6
+
7
+ from localstack_cli import config
8
+ try:
9
+ from localstack_cli.runtime.shutdown import SHUTDOWN_HANDLERS
10
+ except ImportError:
11
+ SHUTDOWN_HANDLERS = None
12
+ from localstack_cli.utils import analytics
13
+ try:
14
+ from localstack_cli.utils.scheduler import Scheduler
15
+ except ImportError:
16
+ Scheduler = None
17
+
18
+ LOG = logging.getLogger(__name__)
19
+
20
+ DEFAULT_FLUSH_INTERVAL_SECS = 15
21
+ EVENT_NAME = "aws_request_agg"
22
+ OPTIONAL_FIELDS = ["err_type"]
23
+
24
+
25
+ class ServiceRequestInfo(NamedTuple):
26
+ service: str
27
+ operation: str
28
+ status_code: int
29
+ err_type: str | None = None
30
+
31
+
32
+ class ServiceRequestAggregator:
33
+ """
34
+ Collects API call data, aggregates it into small batches, and periodically emits (flushes) it as an
35
+ analytics event.
36
+ """
37
+
38
+ def __init__(self, flush_interval: float = DEFAULT_FLUSH_INTERVAL_SECS):
39
+ self.counter = Counter()
40
+ self._flush_interval = flush_interval
41
+ self._flush_scheduler = Scheduler()
42
+ self._mutex = threading.RLock()
43
+ self._period_start_time = datetime.datetime.now(datetime.UTC)
44
+ self._is_started = False
45
+ self._is_shutdown = False
46
+
47
+ def start(self):
48
+ """
49
+ Start a thread that periodically flushes HTTP response data aggregations as analytics events
50
+ :returns: the thread containing the running flush scheduler
51
+ """
52
+ with self._mutex:
53
+ if self._is_started:
54
+ return
55
+ self._is_started = True
56
+
57
+ # schedule flush task
58
+ self._flush_scheduler.schedule(
59
+ func=self._flush, period=self._flush_interval, fixed_rate=True
60
+ )
61
+
62
+ # start thread
63
+ _flush_scheduler_thread = threading.Thread(
64
+ target=self._flush_scheduler.run, daemon=True
65
+ )
66
+ _flush_scheduler_thread.start()
67
+
68
+ SHUTDOWN_HANDLERS.register(self.shutdown)
69
+
70
+ def shutdown(self):
71
+ with self._mutex:
72
+ if not self._is_started:
73
+ return
74
+ if self._is_shutdown:
75
+ return
76
+ self._is_shutdown = True
77
+
78
+ self._flush()
79
+ self._flush_scheduler.close()
80
+ SHUTDOWN_HANDLERS.unregister(self.shutdown)
81
+
82
+ def add_request(self, request_info: ServiceRequestInfo):
83
+ """
84
+ Add an API call for aggregation and collection.
85
+
86
+ :param request_info: information about the API call.
87
+ """
88
+ if config.DISABLE_EVENTS:
89
+ return
90
+
91
+ if self._is_shutdown:
92
+ return
93
+
94
+ with self._mutex:
95
+ self.counter[request_info] += 1
96
+
97
+ def _flush(self):
98
+ """
99
+ Flushes the current batch of HTTP response data as an analytics event.
100
+ This happens automatically in the background.
101
+ """
102
+ with self._mutex:
103
+ try:
104
+ if len(self.counter) == 0:
105
+ return
106
+ analytics_payload = self._create_analytics_payload()
107
+ self._emit_payload(analytics_payload)
108
+ self.counter.clear()
109
+ finally:
110
+ self._period_start_time = datetime.datetime.now(datetime.UTC)
111
+
112
+ def _create_analytics_payload(self):
113
+ return {
114
+ "period_start_time": self._period_start_time.isoformat().replace("+00:00", "Z"),
115
+ "period_end_time": datetime.datetime.now(datetime.UTC)
116
+ .isoformat()
117
+ .replace("+00:00", "Z"),
118
+ "api_calls": self._aggregate_api_calls(self.counter),
119
+ }
120
+
121
+ def _aggregate_api_calls(self, counter) -> list:
122
+ aggregations = []
123
+ for api_call_info, count in counter.items():
124
+ doc = api_call_info._asdict()
125
+ for field in OPTIONAL_FIELDS:
126
+ if doc.get(field) is None:
127
+ del doc[field]
128
+ doc["count"] = count
129
+ aggregations.append(doc)
130
+ return aggregations
131
+
132
+ def _emit_payload(self, analytics_payload: dict):
133
+ analytics.log.event(EVENT_NAME, analytics_payload)