provide-foundation 0.0.0.dev0__py3-none-any.whl → 0.0.0.dev2__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 (161) hide show
  1. provide/foundation/__init__.py +41 -23
  2. provide/foundation/archive/__init__.py +23 -0
  3. provide/foundation/archive/base.py +70 -0
  4. provide/foundation/archive/bzip2.py +157 -0
  5. provide/foundation/archive/gzip.py +159 -0
  6. provide/foundation/archive/operations.py +334 -0
  7. provide/foundation/archive/tar.py +164 -0
  8. provide/foundation/archive/zip.py +203 -0
  9. provide/foundation/cli/__init__.py +2 -2
  10. provide/foundation/cli/commands/deps.py +13 -7
  11. provide/foundation/cli/commands/logs/__init__.py +1 -1
  12. provide/foundation/cli/commands/logs/query.py +1 -1
  13. provide/foundation/cli/commands/logs/send.py +1 -1
  14. provide/foundation/cli/commands/logs/tail.py +1 -1
  15. provide/foundation/cli/decorators.py +11 -10
  16. provide/foundation/cli/main.py +1 -1
  17. provide/foundation/cli/testing.py +2 -35
  18. provide/foundation/cli/utils.py +21 -17
  19. provide/foundation/config/__init__.py +35 -2
  20. provide/foundation/config/base.py +2 -2
  21. provide/foundation/config/converters.py +479 -0
  22. provide/foundation/config/defaults.py +67 -0
  23. provide/foundation/config/env.py +4 -19
  24. provide/foundation/config/loader.py +9 -3
  25. provide/foundation/config/sync.py +19 -4
  26. provide/foundation/console/input.py +5 -5
  27. provide/foundation/console/output.py +35 -13
  28. provide/foundation/context/__init__.py +8 -4
  29. provide/foundation/context/core.py +85 -109
  30. provide/foundation/core.py +1 -2
  31. provide/foundation/crypto/__init__.py +2 -0
  32. provide/foundation/crypto/certificates/__init__.py +34 -0
  33. provide/foundation/crypto/certificates/base.py +173 -0
  34. provide/foundation/crypto/certificates/certificate.py +290 -0
  35. provide/foundation/crypto/certificates/factory.py +213 -0
  36. provide/foundation/crypto/certificates/generator.py +138 -0
  37. provide/foundation/crypto/certificates/loader.py +130 -0
  38. provide/foundation/crypto/certificates/operations.py +198 -0
  39. provide/foundation/crypto/certificates/trust.py +107 -0
  40. provide/foundation/errors/__init__.py +2 -3
  41. provide/foundation/errors/decorators.py +0 -231
  42. provide/foundation/errors/types.py +0 -97
  43. provide/foundation/eventsets/__init__.py +0 -0
  44. provide/foundation/eventsets/display.py +84 -0
  45. provide/foundation/eventsets/registry.py +160 -0
  46. provide/foundation/eventsets/resolver.py +192 -0
  47. provide/foundation/eventsets/sets/das.py +128 -0
  48. provide/foundation/eventsets/sets/database.py +125 -0
  49. provide/foundation/eventsets/sets/http.py +153 -0
  50. provide/foundation/eventsets/sets/llm.py +139 -0
  51. provide/foundation/eventsets/sets/task_queue.py +107 -0
  52. provide/foundation/eventsets/types.py +70 -0
  53. provide/foundation/file/directory.py +13 -22
  54. provide/foundation/file/lock.py +3 -1
  55. provide/foundation/hub/components.py +77 -515
  56. provide/foundation/hub/config.py +151 -0
  57. provide/foundation/hub/discovery.py +62 -0
  58. provide/foundation/hub/handlers.py +81 -0
  59. provide/foundation/hub/lifecycle.py +194 -0
  60. provide/foundation/hub/manager.py +4 -4
  61. provide/foundation/hub/processors.py +44 -0
  62. provide/foundation/integrations/__init__.py +11 -0
  63. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  64. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  65. provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
  66. provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
  67. provide/foundation/integrations/openobserve/config.py +37 -0
  68. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
  70. provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
  71. provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
  72. provide/foundation/logger/__init__.py +3 -10
  73. provide/foundation/logger/config/logging.py +68 -298
  74. provide/foundation/logger/config/telemetry.py +41 -121
  75. provide/foundation/logger/core.py +0 -2
  76. provide/foundation/logger/custom_processors.py +1 -0
  77. provide/foundation/logger/factories.py +11 -2
  78. provide/foundation/logger/processors/main.py +20 -84
  79. provide/foundation/logger/setup/__init__.py +5 -1
  80. provide/foundation/logger/setup/coordinator.py +76 -24
  81. provide/foundation/logger/setup/processors.py +2 -9
  82. provide/foundation/logger/trace.py +27 -0
  83. provide/foundation/metrics/otel.py +10 -10
  84. provide/foundation/observability/__init__.py +2 -2
  85. provide/foundation/process/__init__.py +9 -0
  86. provide/foundation/process/exit.py +47 -0
  87. provide/foundation/process/lifecycle.py +115 -59
  88. provide/foundation/resilience/__init__.py +35 -0
  89. provide/foundation/resilience/circuit.py +164 -0
  90. provide/foundation/resilience/decorators.py +220 -0
  91. provide/foundation/resilience/fallback.py +193 -0
  92. provide/foundation/resilience/retry.py +325 -0
  93. provide/foundation/streams/config.py +79 -0
  94. provide/foundation/streams/console.py +7 -8
  95. provide/foundation/streams/core.py +6 -3
  96. provide/foundation/streams/file.py +12 -2
  97. provide/foundation/testing/__init__.py +84 -2
  98. provide/foundation/testing/archive/__init__.py +24 -0
  99. provide/foundation/testing/archive/fixtures.py +217 -0
  100. provide/foundation/testing/cli.py +30 -17
  101. provide/foundation/testing/common/__init__.py +32 -0
  102. provide/foundation/testing/common/fixtures.py +236 -0
  103. provide/foundation/testing/file/__init__.py +40 -0
  104. provide/foundation/testing/file/content_fixtures.py +316 -0
  105. provide/foundation/testing/file/directory_fixtures.py +107 -0
  106. provide/foundation/testing/file/fixtures.py +52 -0
  107. provide/foundation/testing/file/special_fixtures.py +153 -0
  108. provide/foundation/testing/logger.py +117 -11
  109. provide/foundation/testing/mocking/__init__.py +46 -0
  110. provide/foundation/testing/mocking/fixtures.py +331 -0
  111. provide/foundation/testing/process/__init__.py +48 -0
  112. provide/foundation/testing/process/async_fixtures.py +405 -0
  113. provide/foundation/testing/process/fixtures.py +56 -0
  114. provide/foundation/testing/process/subprocess_fixtures.py +209 -0
  115. provide/foundation/testing/threading/__init__.py +38 -0
  116. provide/foundation/testing/threading/basic_fixtures.py +101 -0
  117. provide/foundation/testing/threading/data_fixtures.py +99 -0
  118. provide/foundation/testing/threading/execution_fixtures.py +263 -0
  119. provide/foundation/testing/threading/fixtures.py +54 -0
  120. provide/foundation/testing/threading/sync_fixtures.py +97 -0
  121. provide/foundation/testing/time/__init__.py +32 -0
  122. provide/foundation/testing/time/fixtures.py +409 -0
  123. provide/foundation/testing/transport/__init__.py +30 -0
  124. provide/foundation/testing/transport/fixtures.py +280 -0
  125. provide/foundation/tools/__init__.py +58 -0
  126. provide/foundation/tools/base.py +348 -0
  127. provide/foundation/tools/cache.py +268 -0
  128. provide/foundation/tools/downloader.py +224 -0
  129. provide/foundation/tools/installer.py +254 -0
  130. provide/foundation/tools/registry.py +223 -0
  131. provide/foundation/tools/resolver.py +321 -0
  132. provide/foundation/tools/verifier.py +186 -0
  133. provide/foundation/tracer/otel.py +7 -11
  134. provide/foundation/tracer/spans.py +2 -2
  135. provide/foundation/transport/__init__.py +155 -0
  136. provide/foundation/transport/base.py +171 -0
  137. provide/foundation/transport/client.py +266 -0
  138. provide/foundation/transport/config.py +140 -0
  139. provide/foundation/transport/errors.py +79 -0
  140. provide/foundation/transport/http.py +232 -0
  141. provide/foundation/transport/middleware.py +360 -0
  142. provide/foundation/transport/registry.py +167 -0
  143. provide/foundation/transport/types.py +45 -0
  144. provide/foundation/utils/deps.py +14 -12
  145. provide/foundation/utils/parsing.py +49 -4
  146. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
  147. provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
  148. provide/foundation/cli/commands/logs/generate_old.py +0 -569
  149. provide/foundation/crypto/certificates.py +0 -896
  150. provide/foundation/logger/emoji/__init__.py +0 -44
  151. provide/foundation/logger/emoji/matrix.py +0 -209
  152. provide/foundation/logger/emoji/sets.py +0 -458
  153. provide/foundation/logger/emoji/types.py +0 -56
  154. provide/foundation/logger/setup/emoji_resolver.py +0 -64
  155. provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
  156. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  157. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  158. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
  159. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
  160. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
  161. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/top_level.txt +0 -0
@@ -1,569 +0,0 @@
1
- """
2
- Generate test logs command for Foundation CLI.
3
-
4
- Incorporates creative prose inspired by William S. Burroughs and the cut-up technique.
5
- """
6
-
7
- import random
8
- import time
9
- from typing import Any
10
-
11
- try:
12
- import click
13
-
14
- _HAS_CLICK = True
15
- except ImportError:
16
- click = None
17
- _HAS_CLICK = False
18
-
19
- from provide.foundation.logger import get_logger
20
-
21
- log = get_logger(__name__)
22
-
23
-
24
- # Cut-up phrases inspired by Burroughs
25
- BURROUGHS_PHRASES = [
26
- "mutated Soft Machine prescribed within data stream",
27
- "pre-recorded talking asshole dissolved into under neon hum",
28
- "the viral Word carrying a new strain of reality",
29
- "equations of control flickering on a broken monitor",
30
- "memory banks spilling future-pasts onto the terminal floor",
31
- "a thousand junk units screaming in unison",
32
- "the algebra of need computed by the Nova Mob",
33
- "subliminal commands embedded in the white noise",
34
- "the Biologic Courts passing sentence in a dream",
35
- "Nova Police raiding the reality studio",
36
- "the soft typewriter of the Other Half",
37
- "a flickering hologram of Hassan i Sabbah",
38
- "contaminated data feed from the Crab Nebula",
39
- "thought-forms materializing in the Interzone",
40
- "frequency shift reported by Sector 5",
41
- ]
42
-
43
- # Standard technical messages
44
- TECHNICAL_MESSAGES = [
45
- "Request processed successfully",
46
- "Database connection established",
47
- "Cache invalidated",
48
- "User authenticated",
49
- "Session initiated",
50
- "Transaction completed",
51
- "Queue message processed",
52
- "Health check passed",
53
- "Metrics exported",
54
- "Configuration reloaded",
55
- "Backup completed",
56
- "Index rebuilt",
57
- "Connection pool recycled",
58
- "Rate limit enforced",
59
- "Circuit breaker triggered",
60
- ]
61
-
62
- # Services and operations for realistic logs
63
- SERVICES = [
64
- "api-gateway",
65
- "auth-service",
66
- "payment-processor",
67
- "user-service",
68
- "notification-engine",
69
- "data-pipeline",
70
- "cache-layer",
71
- "search-index",
72
- "reality-studio",
73
- "interzone-terminal",
74
- "nova-police",
75
- "soft-machine",
76
- ]
77
-
78
- OPERATIONS = [
79
- "handle_request",
80
- "process_data",
81
- "validate_input",
82
- "execute_query",
83
- "send_notification",
84
- "update_cache",
85
- "compute_metrics",
86
- "sync_state",
87
- "transmit_signal",
88
- "decode_reality",
89
- "intercept_word",
90
- "scan_frequency",
91
- ]
92
-
93
- DOMAINS = [
94
- "transmission",
95
- "control",
96
- "reality",
97
- "system",
98
- "network",
99
- "quantum",
100
- "temporal",
101
- "dimensional",
102
- "biologic",
103
- "viral",
104
- ]
105
-
106
- ACTIONS = [
107
- "broadcast",
108
- "receive",
109
- "process",
110
- "analyze",
111
- "detect",
112
- "mutate",
113
- "dissolve",
114
- "compute",
115
- "raid",
116
- "intercept",
117
- ]
118
-
119
- STATUSES = [
120
- "nominal",
121
- "degraded",
122
- "critical",
123
- "optimal",
124
- "unstable",
125
- "fluctuating",
126
- "synchronized",
127
- "divergent",
128
- "contaminated",
129
- "clean",
130
- ]
131
-
132
-
133
- if _HAS_CLICK:
134
-
135
- @click.command("generate")
136
- @click.option(
137
- "--count",
138
- "-n",
139
- type=int,
140
- default=100,
141
- help="Number of logs to generate (0 for continuous)",
142
- )
143
- @click.option(
144
- "--rate",
145
- "-r",
146
- type=float,
147
- default=10.0,
148
- help="Target logs per second (can go up to 10000/s)",
149
- )
150
- @click.option(
151
- "--style",
152
- type=click.Choice(["technical", "burroughs", "mixed"]),
153
- default="mixed",
154
- help="Log message style",
155
- )
156
- @click.option(
157
- "--error-rate",
158
- type=float,
159
- default=0.1,
160
- help="Percentage of error logs (0.0 to 1.0)",
161
- )
162
- @click.option(
163
- "--services",
164
- help="Comma-separated list of services (uses defaults if not provided)",
165
- )
166
- @click.option(
167
- "--stream",
168
- default="default",
169
- help="Target stream for logs",
170
- )
171
- @click.option(
172
- "--batch-size",
173
- type=int,
174
- default=10,
175
- help="Number of logs to send in each batch",
176
- )
177
- @click.option(
178
- "--with-traces",
179
- is_flag=True,
180
- default=True,
181
- help="Generate trace IDs for correlation",
182
- )
183
- @click.pass_context
184
- def generate_command(
185
- ctx, count, rate, style, error_rate, services, stream, batch_size, with_traces
186
- ):
187
- """Generate test logs with optional Burroughs-inspired prose.
188
-
189
- Examples:
190
- # Generate 100 test logs
191
- foundation logs generate -n 100
192
-
193
- # Generate continuous logs at 5/second
194
- foundation logs generate -n 0 -r 5
195
-
196
- # Generate with Burroughs-style messages
197
- foundation logs generate --style burroughs
198
-
199
- # Generate with 20% error rate
200
- foundation logs generate --error-rate 0.2
201
-
202
- # Generate for specific services
203
- foundation logs generate --services "api,auth,payment"
204
- """
205
-
206
- client = ctx.obj.get("client")
207
-
208
- # Parse services
209
- if services:
210
- service_list = [s.strip() for s in services.split(",")]
211
- else:
212
- service_list = SERVICES
213
-
214
- click.echo("🚀 Starting log generation...")
215
- click.echo(f" Style: {style}")
216
- click.echo(f" Error rate: {error_rate * 100:.0f}%")
217
- click.echo(f" Target stream: {stream}")
218
- if count == 0:
219
- click.echo(f" Mode: Continuous at {rate} logs/second")
220
- else:
221
- click.echo(f" Count: {count} logs")
222
- click.echo(" Press Ctrl+C to stop\n")
223
-
224
- def generate_message(style: str, index: int) -> tuple[str, str]:
225
- """Generate a log message based on style."""
226
- if style == "burroughs":
227
- message = random.choice(BURROUGHS_PHRASES)
228
- level = random.choice(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"])
229
- elif style == "technical":
230
- message = random.choice(TECHNICAL_MESSAGES)
231
- level = random.choice(["DEBUG", "INFO", "WARN"] * 3 + ["ERROR"])
232
- else: # mixed
233
- if random.random() > 0.7:
234
- message = random.choice(BURROUGHS_PHRASES)
235
- level = random.choice(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"])
236
- else:
237
- message = random.choice(TECHNICAL_MESSAGES)
238
- level = random.choice(["DEBUG", "INFO", "WARN"] * 3 + ["ERROR"])
239
-
240
- # Override level based on error rate
241
- if random.random() < error_rate:
242
- level = "ERROR"
243
- if style != "burroughs":
244
- message = f"Error: {message}"
245
-
246
- return message, level
247
-
248
- def generate_log_entry(index: int) -> dict[str, Any]:
249
- """Generate a single log entry."""
250
- message, level = generate_message(style, index)
251
- service = random.choice(service_list)
252
- operation = random.choice(OPERATIONS)
253
-
254
- entry = {
255
- "message": f"[{service}] {message}",
256
- "level": level,
257
- "service": service,
258
- "operation": operation,
259
- "domain": random.choice(DOMAINS),
260
- "action": random.choice(ACTIONS),
261
- "status": "degraded" if level == "ERROR" else random.choice(STATUSES),
262
- "duration_ms": random.randint(10, 5000),
263
- "iteration": index,
264
- }
265
-
266
- # Add trace correlation
267
- if with_traces:
268
- # Group logs by trace (5-10 logs per trace)
269
- trace_group = index // random.randint(5, 10)
270
- entry["trace_id"] = f"trace_{trace_group:08x}"
271
- entry["span_id"] = f"span_{index:08x}"
272
-
273
- # Add error details
274
- if level == "ERROR":
275
- entry["error_code"] = random.choice([400, 404, 500, 502, 503])
276
- entry["error_type"] = random.choice(
277
- [
278
- "ConnectionTimeout",
279
- "ValidationError",
280
- "DatabaseError",
281
- "ServiceUnavailable",
282
- ]
283
- )
284
-
285
- return entry
286
-
287
- try:
288
- logs_sent = 0
289
- logs_failed = 0
290
- logs_rate_limited = 0
291
- start_time = time.time()
292
- last_stats_time = start_time
293
- last_stats_sent = 0
294
-
295
- # Set up rate limiter
296
- # Configure with the target rate and a reasonable burst size
297
- rate_limiter = SimpleSyncRateLimiter(
298
- capacity=min(
299
- rate * 2, 1000
300
- ), # Allow burst up to 2 seconds worth or 1000
301
- refill_rate=rate, # tokens per second
302
- )
303
-
304
- # Track rate limiting
305
- rate_limit_detected = False
306
- consecutive_failures = 0
307
-
308
- def send_log_with_tracking(entry):
309
- """Send log using Foundation's logger and track success/failure."""
310
- nonlocal \
311
- logs_sent, \
312
- logs_failed, \
313
- logs_rate_limited, \
314
- rate_limit_detected, \
315
- consecutive_failures
316
-
317
- try:
318
- # Get a logger for the service
319
- service_logger = get_logger(f"generated.{entry['service']}")
320
-
321
- # Use Foundation's logger with appropriate level
322
- level = entry["level"].lower()
323
- message = entry["message"]
324
-
325
- # Remove message and level from attributes since they're passed separately
326
- attrs = {
327
- k: v for k, v in entry.items() if k not in ["message", "level"]
328
- }
329
-
330
- # Call the appropriate log level method
331
- if level == "trace":
332
- service_logger.trace(message, **attrs)
333
- elif level == "debug":
334
- service_logger.debug(message, **attrs)
335
- elif level == "info":
336
- service_logger.info(message, **attrs)
337
- elif level == "warn" or level == "warning":
338
- service_logger.warning(message, **attrs)
339
- elif level == "error":
340
- service_logger.error(message, **attrs)
341
- elif level == "critical":
342
- service_logger.critical(message, **attrs)
343
- else:
344
- service_logger.info(message, **attrs)
345
-
346
- # Also send to OpenObserve if configured
347
- if client:
348
- from provide.foundation.observability.openobserve.otlp import (
349
- send_log_bulk,
350
- )
351
-
352
- success = send_log_bulk(
353
- message=message,
354
- level=entry["level"],
355
- service=entry["service"],
356
- attributes=attrs,
357
- client=client,
358
- )
359
- if not success:
360
- logs_failed += 1
361
- consecutive_failures += 1
362
- if consecutive_failures >= 5 and not rate_limit_detected:
363
- rate_limit_detected = True
364
- logs_rate_limited = logs_failed
365
- elif rate_limit_detected:
366
- logs_rate_limited += 1
367
- return False
368
-
369
- logs_sent += 1
370
- consecutive_failures = 0
371
- return True
372
-
373
- except Exception as e:
374
- log.debug(f"Failed to send log: {e}")
375
- logs_failed += 1
376
- consecutive_failures += 1
377
- return False
378
-
379
- if count == 0:
380
- # Continuous mode using Foundation's rate limiter with async workers
381
- index = 0
382
- import concurrent.futures
383
- import queue
384
-
385
- # Work queue for async processing
386
- work_queue = queue.Queue(
387
- maxsize=int(rate * 2)
388
- ) # Buffer up to 2 seconds of logs
389
-
390
- # Use thread pool for high-speed sending
391
- # More workers for higher rates
392
- num_workers = min(50, max(4, int(rate / 100)))
393
-
394
- with concurrent.futures.ThreadPoolExecutor(
395
- max_workers=num_workers
396
- ) as executor:
397
- # Start worker threads
398
- futures = []
399
-
400
- def worker():
401
- """Worker thread that processes logs from queue."""
402
- while True:
403
- try:
404
- entry = work_queue.get(timeout=1)
405
- if entry is None: # Shutdown signal
406
- break
407
- send_log_with_tracking(entry)
408
- work_queue.task_done()
409
- except queue.Empty:
410
- continue
411
-
412
- # Start workers
413
- for _ in range(num_workers):
414
- futures.append(executor.submit(worker))
415
-
416
- while True:
417
- current_time = time.time()
418
-
419
- # Try to acquire a token from the rate limiter
420
- if rate_limiter.acquire():
421
- # Token acquired, generate and queue log
422
- entry = generate_log_entry(index)
423
- try:
424
- work_queue.put_nowait(entry)
425
- index += 1
426
- except queue.Full:
427
- # Queue is full, we're generating faster than sending
428
- logs_rate_limited += 1
429
- if not rate_limit_detected:
430
- rate_limit_detected = True
431
- log.warning(
432
- "⚠️ Queue full - cannot keep up with target rate"
433
- )
434
- else:
435
- # Rate limited - track it
436
- logs_rate_limited += 1
437
- if not rate_limit_detected:
438
- rate_limit_detected = True
439
- log.debug(
440
- "⚠️ Rate limiter activated - target rate exceeded"
441
- )
442
-
443
- # Small sleep to prevent busy waiting
444
- time.sleep(0.0001)
445
-
446
- # Print stats every second
447
- if current_time - last_stats_time >= 1.0:
448
- current_sent = logs_sent
449
- current_rate = (current_sent - last_stats_sent) / (
450
- current_time - last_stats_time
451
- )
452
- tokens_available = rate_limiter.tokens
453
- queue_size = work_queue.qsize()
454
-
455
- status = f"📊 Sent: {logs_sent:,} | Rate: {current_rate:.0f}/s | Tokens: {tokens_available:.0f}/{rate_limiter.capacity} | Queue: {queue_size}"
456
- if logs_failed > 0:
457
- status += f" | Failed: {logs_failed:,}"
458
- if rate_limit_detected:
459
- status += f" | ⚠️ RATE LIMITED ({logs_rate_limited:,} throttled)"
460
-
461
- click.echo(status)
462
- last_stats_time = current_time
463
- last_stats_sent = current_sent
464
-
465
- else:
466
- # Fixed count mode using Foundation's rate limiter with async workers
467
- import concurrent.futures
468
- import queue
469
-
470
- # Work queue for async processing
471
- work_queue = queue.Queue(maxsize=min(1000, count))
472
-
473
- # Use thread pool for high-speed sending
474
- num_workers = min(50, max(4, int(rate / 100)))
475
-
476
- with concurrent.futures.ThreadPoolExecutor(
477
- max_workers=num_workers
478
- ) as executor:
479
- # Start worker threads
480
- def worker():
481
- """Worker thread that processes logs from queue."""
482
- while True:
483
- try:
484
- entry = work_queue.get(timeout=1)
485
- if entry is None: # Shutdown signal
486
- break
487
- send_log_with_tracking(entry)
488
- work_queue.task_done()
489
- except queue.Empty:
490
- continue
491
-
492
- # Start workers
493
- workers = [executor.submit(worker) for _ in range(num_workers)]
494
-
495
- # Generate and queue logs
496
- for i in range(count):
497
- # Wait for rate limiter token
498
- while not rate_limiter.acquire():
499
- logs_rate_limited += 1
500
- if not rate_limit_detected:
501
- rate_limit_detected = True
502
- log.debug(
503
- "⚠️ Rate limiter activated - target rate exceeded"
504
- )
505
- time.sleep(
506
- 0.0001
507
- ) # Very small sleep to prevent busy waiting
508
-
509
- # Generate and queue log
510
- entry = generate_log_entry(i)
511
- work_queue.put(entry)
512
-
513
- # Print progress
514
- if (i + 1) % 100 == 0:
515
- current_time = time.time()
516
- elapsed = current_time - start_time
517
- current_rate = logs_sent / elapsed if elapsed > 0 else 0
518
- tokens_available = rate_limiter.tokens
519
- queue_size = work_queue.qsize()
520
-
521
- status = f"📊 Progress: {i + 1}/{count} | Sent: {logs_sent:,} | Rate: {current_rate:.0f}/s | Queue: {queue_size} | Tokens: {tokens_available:.0f}"
522
- if logs_failed > 0:
523
- status += f" | Failed: {logs_failed}"
524
- if rate_limit_detected:
525
- status += f" | ⚠️ THROTTLED ({logs_rate_limited:,})"
526
-
527
- click.echo(status)
528
-
529
- # Wait for queue to empty
530
- click.echo("⏳ Waiting for queue to empty...")
531
- work_queue.join()
532
-
533
- # Shutdown workers
534
- for _ in range(num_workers):
535
- work_queue.put(None)
536
-
537
- # Wait for workers to finish
538
- concurrent.futures.wait(workers)
539
-
540
- elapsed = time.time() - start_time
541
- rate_actual = logs_sent / elapsed if elapsed > 0 else 0
542
-
543
- click.echo("\n📊 Generation complete:")
544
- click.echo(f" Total sent: {logs_sent:,} logs")
545
- click.echo(f" Total failed: {logs_failed:,} logs")
546
- if rate_limit_detected:
547
- click.echo(f" ⚠️ Rate limited: {logs_rate_limited:,} logs")
548
- click.echo(f" Time: {elapsed:.2f}s")
549
- click.echo(f" Target rate: {rate:.0f} logs/second")
550
- click.echo(f" Actual rate: {rate_actual:.1f} logs/second")
551
- if rate_limit_detected and rate_actual < rate * 0.5:
552
- click.echo(
553
- f" ⚠️ Rate limiting detected - actual rate is {(rate_actual / rate) * 100:.0f}% of target"
554
- )
555
-
556
- except KeyboardInterrupt:
557
- click.echo(f"\n✋ Stopped. Generated {logs_sent} logs.")
558
- except Exception as e:
559
- click.echo(f"Generation failed: {e}", err=True)
560
- return 1
561
-
562
- else:
563
-
564
- def generate_command(*args, **kwargs):
565
- """Generate command stub when click is not available."""
566
- raise ImportError(
567
- "CLI commands require optional dependencies. "
568
- "Install with: pip install 'provide-foundation[cli]'"
569
- )