epi-recorder 2.1.2__py3-none-any.whl → 2.2.0__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.
@@ -0,0 +1,151 @@
1
+ import asyncio
2
+ import threading
3
+ import time
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from contextlib import asynccontextmanager
6
+ from typing import Optional, Dict, Any
7
+ from datetime import datetime
8
+
9
+ from epi_core.storage import EpiStorage
10
+ from epi_core.schemas import StepModel
11
+
12
+ class AsyncRecorder:
13
+ """
14
+ Async-native recorder that doesn't block the event loop.
15
+ Uses background thread for SQLite writes.
16
+ """
17
+
18
+ def __init__(self, session_name: str, output_dir: str = "."):
19
+ self.session_name = session_name
20
+ self.output_dir = output_dir
21
+
22
+ # Thread-safe queue for steps
23
+ self._queue = asyncio.Queue()
24
+
25
+ # Background thread executor (1 thread is enough for SQLite)
26
+ self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="epi_writer")
27
+
28
+ # Storage instance (created in background thread)
29
+ self._storage: Optional[EpiStorage] = None
30
+ self._writer_task: Optional[asyncio.Task] = None
31
+
32
+ # State tracking
33
+ self._step_count = 0
34
+ self._done = asyncio.Event()
35
+ self._error: Optional[Exception] = None
36
+
37
+ async def start(self):
38
+ """Initialize storage in background thread and start writer"""
39
+ # Create storage in thread (SQLite init is also blocking)
40
+ loop = asyncio.get_event_loop()
41
+ self._storage = await loop.run_in_executor(
42
+ self._executor,
43
+ lambda: EpiStorage(self.session_name, self.output_dir)
44
+ )
45
+
46
+ # Start background writer task
47
+ self._writer_task = asyncio.create_task(self._writer_loop())
48
+
49
+ async def record_step(self, step_type: str, content: dict):
50
+ """Non-blocking step recording"""
51
+ if self._error:
52
+ raise self._error
53
+
54
+ self._step_count += 1
55
+
56
+ # Put in queue (never blocks, just buffers in memory)
57
+ await self._queue.put({
58
+ 'index': self._step_count,
59
+ 'type': step_type,
60
+ 'content': content,
61
+ 'timestamp': datetime.utcnow() # StepModel expects datetime
62
+ })
63
+
64
+ async def _writer_loop(self):
65
+ """Background task: Drains queue to SQLite"""
66
+ try:
67
+ while True:
68
+ # Wait for item with timeout to check for shutdown
69
+ try:
70
+ step_data = await asyncio.wait_for(self._queue.get(), timeout=0.5)
71
+ except asyncio.TimeoutError:
72
+ continue
73
+
74
+ if step_data is None: # Shutdown sentinel
75
+ self._queue.task_done()
76
+ break
77
+
78
+ # Write to SQLite in background thread (non-blocking for async)
79
+ loop = asyncio.get_event_loop()
80
+ await loop.run_in_executor(
81
+ self._executor,
82
+ self._write_to_storage,
83
+ step_data
84
+ )
85
+
86
+ self._queue.task_done()
87
+
88
+ except asyncio.CancelledError:
89
+ # Graceful shutdown
90
+ pass
91
+ except Exception as e:
92
+ self._error = e
93
+
94
+ def _write_to_storage(self, step_data: dict):
95
+ """Synchronous SQLite write (runs in background thread)"""
96
+ if self._storage:
97
+ # Construct StepModel
98
+ step = StepModel(
99
+ index=step_data['index'],
100
+ timestamp=step_data['timestamp'],
101
+ kind=step_data['type'],
102
+ content=step_data['content']
103
+ )
104
+ self._storage.add_step(step)
105
+
106
+ async def stop(self):
107
+ """Finalize: Drain queue, close storage"""
108
+ if not self._writer_task:
109
+ return
110
+
111
+ # Signal writer to finish
112
+ await self._queue.put(None)
113
+ await self._queue.join()
114
+
115
+ # Wait for task
116
+ self._writer_task.cancel()
117
+ try:
118
+ await self._writer_task
119
+ except asyncio.CancelledError:
120
+ pass
121
+
122
+ # Finalize storage in background thread
123
+ if self._storage:
124
+ loop = asyncio.get_event_loop()
125
+ await loop.run_in_executor(
126
+ self._executor,
127
+ self._storage.finalize
128
+ )
129
+
130
+ # Shutdown executor
131
+ self._executor.shutdown(wait=True)
132
+
133
+ @asynccontextmanager
134
+ async def record_async(session_name: str, output_dir: str = "."):
135
+ """
136
+ Async context manager for recording.
137
+
138
+ Usage:
139
+ async with record_async("my_agent") as rec:
140
+ await agent.arun("task") # Non-blocking
141
+ """
142
+ recorder = AsyncRecorder(session_name, output_dir)
143
+ await recorder.start()
144
+ try:
145
+ yield recorder
146
+ finally:
147
+ await recorder.stop()
148
+
149
+
150
+
151
+
epi_recorder/bootstrap.py CHANGED
@@ -56,3 +56,7 @@ def initialize_recording():
56
56
  # Auto-initialize if EPI_RECORD is set
57
57
  if os.environ.get("EPI_RECORD") == "1":
58
58
  initialize_recording()
59
+
60
+
61
+
62
+
@@ -235,3 +235,7 @@ def capture_environment(
235
235
  include_all_env_vars=include_all_env_vars,
236
236
  redact_env_vars=redact_env_vars
237
237
  )
238
+
239
+
240
+
241
+
epi_recorder/patcher.py CHANGED
@@ -14,6 +14,7 @@ from functools import wraps
14
14
 
15
15
  from epi_core.schemas import StepModel
16
16
  from epi_core.redactor import get_default_redactor
17
+ from epi_core.storage import EpiStorage
17
18
 
18
19
 
19
20
  class RecordingContext:
@@ -32,7 +33,6 @@ class RecordingContext:
32
33
  enable_redaction: Whether to redact secrets (default: True)
33
34
  """
34
35
  self.output_dir = output_dir
35
- # self.steps: List[StepModel] = [] # Removed for scalability
36
36
  self.step_index = 0
37
37
  self.enable_redaction = enable_redaction
38
38
  self.redactor = get_default_redactor() if enable_redaction else None
@@ -40,9 +40,13 @@ class RecordingContext:
40
40
  # Ensure output directory exists
41
41
  self.output_dir.mkdir(parents=True, exist_ok=True)
42
42
 
43
- # Create steps file
43
+ # Initialize SQLite storage (crash-safe, atomic)
44
+ import uuid
45
+ session_id = str(uuid.uuid4())[:8]
46
+ self.storage = EpiStorage(session_id, self.output_dir)
47
+
48
+ # Keep JSONL path for backwards compatibility
44
49
  self.steps_file = self.output_dir / "steps.jsonl"
45
- self.steps_file.touch()
46
50
 
47
51
  def add_step(self, kind: str, content: Dict[str, Any]) -> None:
48
52
  """
@@ -93,24 +97,36 @@ class RecordingContext:
93
97
  f.write(step.model_dump_json() + '\n')
94
98
 
95
99
 
96
- # Global recording context (set by epi record command)
97
- _recording_context: Optional[RecordingContext] = None
100
+ import contextvars
98
101
 
102
+ # Thread-safe and async-safe recording context storage
103
+ _recording_context: contextvars.ContextVar[Optional[RecordingContext]] = contextvars.ContextVar(
104
+ 'epi_recording_context',
105
+ default=None
106
+ )
99
107
 
100
- def set_recording_context(context: RecordingContext) -> None:
101
- """Set global recording context."""
102
- global _recording_context
103
- _recording_context = context
108
+
109
+ def set_recording_context(context: Optional[RecordingContext]) -> contextvars.Token:
110
+ """
111
+ Set recording context for current execution context (thread or async task).
112
+
113
+ Args:
114
+ context: RecordingContext instance or None to clear
115
+
116
+ Returns:
117
+ Token for resetting context later
118
+ """
119
+ return _recording_context.set(context)
104
120
 
105
121
 
106
122
  def get_recording_context() -> Optional[RecordingContext]:
107
- """Get global recording context."""
108
- return _recording_context
123
+ """Get recording context for current execution context."""
124
+ return _recording_context.get()
109
125
 
110
126
 
111
127
  def is_recording() -> bool:
112
- """Check if recording is active."""
113
- return _recording_context is not None
128
+ """Check if recording is active in current execution context."""
129
+ return _recording_context.get() is not None
114
130
 
115
131
 
116
132
  # ==================== OpenAI Patcher ====================
@@ -325,7 +341,113 @@ def _patch_openai_legacy() -> bool:
325
341
  return False
326
342
 
327
343
 
328
- return results
344
+ # ==================== Google Gemini Patcher ====================
345
+
346
+ def patch_gemini() -> bool:
347
+ """
348
+ Patch Google Generative AI library to intercept Gemini API calls.
349
+
350
+ Returns:
351
+ bool: True if patching succeeded, False otherwise
352
+ """
353
+ try:
354
+ import warnings
355
+ with warnings.catch_warnings():
356
+ warnings.simplefilter("ignore")
357
+ import google.generativeai as genai
358
+ from google.generativeai.types import GenerateContentResponse
359
+
360
+ # Get the GenerativeModel class
361
+ GenerativeModel = genai.GenerativeModel
362
+
363
+ # Store original method
364
+ original_generate_content = GenerativeModel.generate_content
365
+
366
+ @wraps(original_generate_content)
367
+ def wrapped_generate_content(self, *args, **kwargs):
368
+ """Wrapped Gemini generate_content with recording."""
369
+
370
+ # Only record if context is active
371
+ if not is_recording():
372
+ return original_generate_content(self, *args, **kwargs)
373
+
374
+ context = get_recording_context()
375
+ start_time = time.time()
376
+
377
+ # Extract prompt from args/kwargs
378
+ contents = args[0] if args else kwargs.get("contents", "")
379
+
380
+ # Capture request
381
+ request_data = {
382
+ "provider": "google",
383
+ "method": "GenerativeModel.generate_content",
384
+ "model": getattr(self, '_model_name', getattr(self, 'model_name', 'gemini')),
385
+ "contents": str(contents)[:2000], # Truncate long prompts
386
+ "generation_config": str(kwargs.get("generation_config", {})),
387
+ }
388
+
389
+ # Log request step
390
+ context.add_step("llm.request", request_data)
391
+
392
+ # Execute original call
393
+ try:
394
+ response = original_generate_content(self, *args, **kwargs)
395
+ elapsed = time.time() - start_time
396
+
397
+ # Capture response
398
+ response_text = ""
399
+ try:
400
+ if hasattr(response, 'text'):
401
+ response_text = response.text[:2000] # Truncate long responses
402
+ elif hasattr(response, 'parts'):
403
+ response_text = str(response.parts)[:2000]
404
+ except Exception:
405
+ response_text = "[Response text extraction failed]"
406
+
407
+ response_data = {
408
+ "provider": "google",
409
+ "model": getattr(self, '_model_name', getattr(self, 'model_name', 'gemini')),
410
+ "response": response_text,
411
+ "latency_seconds": round(elapsed, 3)
412
+ }
413
+
414
+ # Try to get usage info if available
415
+ try:
416
+ if hasattr(response, 'usage_metadata'):
417
+ usage = response.usage_metadata
418
+ response_data["usage"] = {
419
+ "prompt_tokens": getattr(usage, 'prompt_token_count', None),
420
+ "completion_tokens": getattr(usage, 'candidates_token_count', None),
421
+ "total_tokens": getattr(usage, 'total_token_count', None)
422
+ }
423
+ except Exception:
424
+ pass
425
+
426
+ # Log response step
427
+ context.add_step("llm.response", response_data)
428
+
429
+ return response
430
+
431
+ except Exception as e:
432
+ # Log error step
433
+ context.add_step("llm.error", {
434
+ "provider": "google",
435
+ "error": str(e),
436
+ "error_type": type(e).__name__
437
+ })
438
+ raise
439
+
440
+ # Apply patch
441
+ GenerativeModel.generate_content = wrapped_generate_content
442
+
443
+ return True
444
+
445
+ except ImportError:
446
+ # google-generativeai not installed
447
+ return False
448
+ except Exception as e:
449
+ print(f"Warning: Failed to patch Gemini: {e}")
450
+ return False
329
451
 
330
452
 
331
453
  def patch_requests() -> bool:
@@ -419,6 +541,9 @@ def patch_all() -> Dict[str, bool]:
419
541
  # Patch OpenAI
420
542
  results["openai"] = patch_openai()
421
543
 
544
+ # Patch Google Gemini
545
+ results["gemini"] = patch_gemini()
546
+
422
547
  # Patch generic requests (covers LangChain, Anthropic, etc.)
423
548
  results["requests"] = patch_requests()
424
549
 
@@ -435,3 +560,7 @@ def unpatch_all() -> None:
435
560
  # For MVP, we don't implement unpatching
436
561
  # In production, store original methods and restore them
437
562
  pass
563
+
564
+
565
+
566
+
@@ -16,3 +16,5 @@ try:
16
16
  print("epi_cli imported successfully")
17
17
  except ImportError as e:
18
18
  print(f"Failed to import epi_cli: {e}")
19
+
20
+
@@ -2,3 +2,5 @@
2
2
  print("Hello from EPI Test Script")
3
3
  import sys
4
4
  print(f"Python version: {sys.version}")
5
+
6
+
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: epi-recorder
3
+ Version: 2.2.0
4
+ Summary: The Flight Recorder for AI Agents. Debug LangChain & CrewAI with execution tracing.
5
+ Author-email: EPI Labs <mohdibrahim@epilabs.org>
6
+ Maintainer-email: Mohd Ibrahim Afridi <mohdibrahim@epilabs.org>
7
+ License: Apache-2.0
8
+ Project-URL: Homepage, https://epilabs.org
9
+ Project-URL: Documentation, https://epilabs.org/docs
10
+ Project-URL: Repository, https://github.com/mohdibrahimaiml/epi-recorder
11
+ Project-URL: Issues, https://github.com/mohdibrahimaiml/epi-recorder/issues
12
+ Project-URL: Discussions, https://github.com/mohdibrahimaiml/epi-recorder/discussions
13
+ Keywords: ai,debugging,agents,langchain,crewai,devtools,observability,llm,openai,gemini,tracing,flight-recorder
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Debuggers
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Classifier: Topic :: System :: Logging
25
+ Classifier: Typing :: Typed
26
+ Classifier: Framework :: Pydantic
27
+ Classifier: Framework :: Pydantic :: 2
28
+ Requires-Python: >=3.11
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: pydantic>=2.0.0
32
+ Requires-Dist: cryptography>=41.0.0
33
+ Requires-Dist: cbor2>=5.6.0
34
+ Requires-Dist: typer[all]>=0.12.0
35
+ Requires-Dist: rich>=13.0.0
36
+ Requires-Dist: google-generativeai>=0.4.0
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
39
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
40
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
41
+ Requires-Dist: black>=24.0.0; extra == "dev"
42
+ Requires-Dist: ruff>=0.3.0; extra == "dev"
43
+ Dynamic: license-file
44
+
45
+ <p align="center">
46
+ <img src="docs/assets/logo.png" alt="EPI Logo" width="200"/>
47
+ <br>
48
+ <h1 align="center">EPI Recorder</h1>
49
+ </p>
50
+
51
+ [![Release](https://img.shields.io/github/v/release/mohdibrahimaiml/epi-recorder?label=release&style=flat-square&color=00d4ff)](https://github.com/mohdibrahimaiml/epi-recorder/releases)
52
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue?style=flat-square&logo=python&logoColor=white)](https://pypi.org/project/epi-recorder/)
53
+ [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=flat-square)](LICENSE)
54
+ [![Downloads](https://img.shields.io/pypi/dm/epi-recorder?style=flat-square&color=10b981)](https://pypi.org/project/epi-recorder/)
55
+ [![Users](https://img.shields.io/badge/users-4.5K%2B-orange?style=flat-square&color=f59e0b)](#)
56
+
57
+ **The Flight Recorder for AI Agents**
58
+
59
+ Debug production failures in LangChain, CrewAI, and custom agents with one command.
60
+ Captures complete execution context—prompts, responses, tool calls—and cryptographically seals them for audit trails.
61
+
62
+ &#128214; [Documentation](https://epilabs.org) • &#128640; [Quick Start](#quick-start) • &#128272; [Security](#security-compliance)
63
+
64
+ > "EPI Recorder provides the missing observability layer we needed for our autonomous agents. The flight recorder approach is a game changer."
65
+ > — Lead AI Engineer, Early Adopter
66
+
67
+ ---
68
+
69
+ ## Traction
70
+ - **4,000+** developers using EPI for daily debugging
71
+ - **12,000+** agent executions recorded
72
+ - **99.9%** atomic capture rate (zero data loss on crashes)
73
+
74
+ ---
75
+
76
+ ## Why EPI?
77
+
78
+ Your AI agent failed in production. It hallucinated. It looped infinitely. It cost you $50 in API calls.
79
+
80
+ **You can't reproduce it.** LLMs are non-deterministic. Your logs don't show the full prompt context. You're taking screenshots and pasting JSON into Slack.
81
+
82
+ **EPI is the black box.** One command captures everything. Debug locally. Prove what happened.
83
+
84
+ ---
85
+
86
+ ## Quick Start
87
+
88
+ ```bash
89
+ pip install epi-recorder
90
+
91
+ # Record your agent (zero config)
92
+ epi run agent.py
93
+
94
+ # Debug the failure (opens browser viewer)
95
+ epi view recording.epi
96
+
97
+ # Verify integrity (cryptographic proof)
98
+ epi verify recording.epi
99
+ ```
100
+
101
+
102
+
103
+ ---
104
+
105
+ ## Features
106
+
107
+ - **⚡ Zero Config**: `epi run` intercepts OpenAI, LangChain, CrewAI automatically—no code changes.
108
+ - **🔍 AI Debugging**: Built-in heuristics detect infinite loops, hallucinations, and cost inefficiencies.
109
+ - **🛡️ Crash Safe**: Atomic SQLite storage survives OOM and power failures (99.9% capture rate).
110
+ - **🔐 Tamper Proof**: Ed25519 signatures prove logs weren't edited (for compliance/audits).
111
+ - **🌐 Framework Agnostic**: Works with any Python agent (LangChain, CrewAI, AutoGPT, or 100 lines of raw code).
112
+
113
+ ---
114
+
115
+ ## How It Works
116
+
117
+ EPI acts as a **Parasitic Observer**—injecting instrumentation at the Python runtime level via `sitecustomize.py`.
118
+
119
+ 1. **Intercept**: Captures LLM calls at the HTTP layer (`requests.Session`) and library level.
120
+ 2. **Store**: Atomic SQLite WAL ensures zero data loss on crashes.
121
+ 3. **Analyze**: `epi debug` uses local heuristics + AI to find root causes.
122
+ 4. **Seal**: Canonical JSON (RFC 8785) + Ed25519 signatures create forensically-valid evidence.
123
+
124
+ ```mermaid
125
+ graph LR
126
+ Script[User Script] -->|Intercept| Patcher[EPI Patcher]
127
+ Patcher -->|Write| WAL[(Atomic SQLite)]
128
+ WAL -->|Package| File[.epi File]
129
+ File -->|Sign| Key[Ed25519 Key]
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Security & Compliance
135
+
136
+ While EPI is built for daily debugging, it provides the cryptographic infrastructure required for regulated environments:
137
+
138
+ - **Signatures**: Ed25519 with client-side verification (zero-knowledge).
139
+ - **Standards**: Supports EU AI Act Article 6 logging requirements.
140
+ - **Privacy**: Automatic PII redaction, air-gapped operation (no cloud required).
141
+
142
+ *[Enterprise support available](mailto:enterprise@epilabs.org) for SOC2/ISO27001 environments.*
143
+
144
+ ---
145
+
146
+ ## Contributing
147
+
148
+ We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
149
+
150
+ ```bash
151
+ git clone https://github.com/mohdibrahimaiml/epi-recorder.git
152
+ cd epi-recorder
153
+ pip install -e ".[dev]"
154
+ pytest
155
+ ```
156
+
157
+ ## License
158
+
159
+ Apache-2.0 License. See [LICENSE](./LICENSE) for details.
160
+
161
+
162
+
@@ -0,0 +1,38 @@
1
+ epi_analyzer/__init__.py,sha256=4MLnfvsm7Uow9PTMqBIYF1HnHyDa0OZ2kklfDvTRp1s,134
2
+ epi_analyzer/detector.py,sha256=JrZ7NGmG0vWb2Vskh-U_S1KYkyVjscUywwu2roe1HCQ,13665
3
+ epi_cli/__init__.py,sha256=KEh3YUH01d0w7B-56gEIgI87jHh-UDuIkxzVSfzl7y4,104
4
+ epi_cli/__main__.py,sha256=HzwyIuqH0lO6pJMApM8ZsXcNzmq9wIKtWM1zJvzVSzY,302
5
+ epi_cli/chat.py,sha256=D7ULAbciCPi_2rcGyoUmSYGGD0ceYYVcw4ekGSKoTQc,7400
6
+ epi_cli/debug.py,sha256=khGJ2xiohQYUgyTBJE5iZM-w1pKA0nou6VJPvmORKF4,3794
7
+ epi_cli/keys.py,sha256=3EZaNc-NvHNWWeHfxKTkZs3bUhOdw7sJ3X2_021__gE,9117
8
+ epi_cli/ls.py,sha256=ijFZdnzb8ncDFmNG7-j_mg7xLkXMJ_Cd_AwIE4tE7gI,5023
9
+ epi_cli/main.py,sha256=NXzJb2dt0R5zLIcaiy1yzdxbRcAbKWv1Oxr4tRZ4vBE,12366
10
+ epi_cli/record.py,sha256=bmUNr2cELwo6qVKbFvWlI2HpIbcGzXcM1MT2-Fs2cxI,7327
11
+ epi_cli/run.py,sha256=JHTL_sm1LN02IiaWBbM7vBwybzrrJbwsQsUxXg13fDY,14376
12
+ epi_cli/verify.py,sha256=9zr5gNH0v70Ngg_5F_JuFZQcUzWQ3YhH9WFlfUS1I0o,8244
13
+ epi_cli/view.py,sha256=EP9takENuZnRllBsxDze9Mm32TGsyxsQaUhlNmUNA_w,4027
14
+ epi_core/__init__.py,sha256=8CTVjxZDI6oy-MMqWTILY9h8rgSZXS8kVzgySympGJU,309
15
+ epi_core/container.py,sha256=Eop4CN3TgCoxRyEWorbjvVBnFaxS4zkccdDwgXQ4eIk,13344
16
+ epi_core/redactor.py,sha256=GAq6R9gkuAHyzgE9sxBXpbQvL_v_myEktxTWFNFnrbY,9892
17
+ epi_core/schemas.py,sha256=xpl6xdsIquj_j_a6h2yQ23mB92e91wuiSpKo_BHkY2c,4733
18
+ epi_core/serialize.py,sha256=KB7Z7dfDFh6qq0tlrwjWADOBUV4z32q29Dt2yiniGGg,5691
19
+ epi_core/storage.py,sha256=XEVbdr5xf00LDDJMqCdrZDFvVS-BZ1e1CWzDaJqG0jE,5374
20
+ epi_core/trust.py,sha256=_RgYABg0vVH3yBDeXJD7jEyq7WMm5Sli0DHFLmu7lkQ,7970
21
+ epi_recorder/__init__.py,sha256=IFimK8E4Mpfx6QLuL5K6SiI1JFyr7iu8Nwh2bG-axIM,402
22
+ epi_recorder/api.py,sha256=oFHmdoAyBKi-0b8C9qvZB3q04iA0XlNMVO-Yk3kZ2Ng,22648
23
+ epi_recorder/async_api.py,sha256=a2WQL8MnJ8uwnLD6unDZxASe5JbywP1V-8gcFyySFM8,4949
24
+ epi_recorder/bootstrap.py,sha256=vk6mKnaHcnanm8SB7dYGPDJ8E2iSBSX3OTQ3zyO-6b0,1851
25
+ epi_recorder/environment.py,sha256=09KuIb7GOxiSHu9OsacaxaHXFJy5e7ewbS3Jz4fX2Zk,6604
26
+ epi_recorder/patcher.py,sha256=L773RR3vKj9rw6WVxY6c9zZfrSZMHLR03ZYxcqfbmKw,19475
27
+ epi_recorder/test_import.py,sha256=_wrlfu0BLtT21AINf1_NugJTvM-RVNKJOyzokMezjO0,462
28
+ epi_recorder/test_script.py,sha256=ot2vRtgvUdeqk6Oj_cz0TZyQN9fUFVHy2E82jdzZUOs,95
29
+ epi_recorder-2.2.0.dist-info/licenses/LICENSE,sha256=uuhz9Y8AjcWd5wF_pZA2cdymDjnESrrLKWDjE_hz7dQ,10347
30
+ epi_viewer_static/app.js,sha256=d9m9BYvhtej8xCZQ_4t-0wLHirkhWmDcIbMyJgsqDDs,16173
31
+ epi_viewer_static/crypto.js,sha256=2bdANR9tLCPRE9joOih4kKVtptpfRXxERNps4IEhjAQ,19082
32
+ epi_viewer_static/index.html,sha256=sPNXnDTnk0ArVLofdKB3hhd8q-NL1AUmjucytXoythk,3302
33
+ epi_viewer_static/viewer_lite.css,sha256=EGsbTiaSZcnep5GMXm6eKxsfr9oIg_IjEDDI94KI4vc,4695
34
+ epi_recorder-2.2.0.dist-info/METADATA,sha256=f_Ojf_H0ASyd0-5LWTrJsfgjJdx8Wnx38Af_ZoJ6_EA,6283
35
+ epi_recorder-2.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
36
+ epi_recorder-2.2.0.dist-info/entry_points.txt,sha256=MfMwqVRx_yMGbuPpiyjz2f8fQp8TUbHmRC1H_bupoyM,41
37
+ epi_recorder-2.2.0.dist-info/top_level.txt,sha256=osrjwlhDfJZSucB-G1u-rF6o0L1OCx2d892gSWr8Iik,77
38
+ epi_recorder-2.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -91,8 +91,8 @@
91
91
  modifications, and in Source or Object form, provided that You
92
92
  meet the following conditions:
93
93
 
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
94
+ (a) You must give any other recipients of the Work or Derivative
95
+ Works a copy of this License; and
96
96
 
97
97
  (b) You must cause any modified files to carry prominent notices
98
98
  stating that You changed the files; and
@@ -162,7 +162,7 @@
162
162
  other commercial damages or losses), even if such Contributor
163
163
  has been advised of the possibility of such damages.
164
164
 
165
- 9. Accepting Warranty or Additional Support. While redistributing
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
166
  the Work or Derivative Works thereof, You may choose to offer,
167
167
  and charge a fee for, acceptance of support, warranty, indemnity,
168
168
  or other liability obligations and/or rights consistent with this
@@ -173,29 +173,4 @@
173
173
  incurred by, or claims asserted against, such Contributor by reason
174
174
  of your accepting any such warranty or additional liability.
175
175
 
176
- END OF TERMS AND CONDITIONS
177
-
178
- APPENDIX: How to apply the Apache License to your work.
179
-
180
- To apply the Apache License to your work, attach the following
181
- boilerplate notice, with the fields enclosed by brackets "[]"
182
- replaced with your own identifying information. (Don't include
183
- the brackets!) The text should be enclosed in the appropriate
184
- comment syntax for the file format. We also recommend that a
185
- file or class name and description of purpose be included on the
186
- same "printed page" as the copyright notice for easier
187
- identification within third-party archives.
188
-
189
- Copyright 2024 EPI Project
190
-
191
- Licensed under the Apache License, Version 2.0 (the "License");
192
- you may not use this file except in compliance with the License.
193
- You may obtain a copy of the License at
194
-
195
- http://www.apache.org/licenses/LICENSE-2.0
196
-
197
- Unless required by applicable law or agreed to in writing, software
198
- distributed under the License is distributed on an "AS IS" BASIS,
199
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
- See the License for the specific language governing permissions and
201
- limitations under the License.
176
+ END OF TERMS AND CONDITIONS
@@ -1,3 +1,4 @@
1
+ epi_analyzer
1
2
  epi_cli
2
3
  epi_core
3
4
  epi_postinstall