api-chain-runner 1.0.0__tar.gz

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 (40) hide show
  1. api_chain_runner-1.0.0/.gitignore +33 -0
  2. api_chain_runner-1.0.0/ARCHITECTURE.md +405 -0
  3. api_chain_runner-1.0.0/FROZENSET_EXPLANATION.md +208 -0
  4. api_chain_runner-1.0.0/PKG-INFO +12 -0
  5. api_chain_runner-1.0.0/README.md +386 -0
  6. api_chain_runner-1.0.0/api_chain_runner/__init__.py +18 -0
  7. api_chain_runner-1.0.0/api_chain_runner/__main__.py +138 -0
  8. api_chain_runner-1.0.0/api_chain_runner/executor.py +285 -0
  9. api_chain_runner-1.0.0/api_chain_runner/generator.py +126 -0
  10. api_chain_runner-1.0.0/api_chain_runner/logger.py +89 -0
  11. api_chain_runner-1.0.0/api_chain_runner/models.py +149 -0
  12. api_chain_runner-1.0.0/api_chain_runner/pause.py +104 -0
  13. api_chain_runner-1.0.0/api_chain_runner/resolver.py +118 -0
  14. api_chain_runner-1.0.0/api_chain_runner/runner.py +416 -0
  15. api_chain_runner-1.0.0/api_chain_runner/store.py +80 -0
  16. api_chain_runner-1.0.0/api_chain_runner.egg-info/PKG-INFO +12 -0
  17. api_chain_runner-1.0.0/api_chain_runner.egg-info/SOURCES.txt +38 -0
  18. api_chain_runner-1.0.0/api_chain_runner.egg-info/dependency_links.txt +1 -0
  19. api_chain_runner-1.0.0/api_chain_runner.egg-info/entry_points.txt +2 -0
  20. api_chain_runner-1.0.0/api_chain_runner.egg-info/requires.txt +7 -0
  21. api_chain_runner-1.0.0/api_chain_runner.egg-info/top_level.txt +1 -0
  22. api_chain_runner-1.0.0/bharatpe.yaml +285 -0
  23. api_chain_runner-1.0.0/doc_file/SELFIE.jpeg +0 -0
  24. api_chain_runner-1.0.0/doc_file/aadhaar.xml +39 -0
  25. api_chain_runner-1.0.0/doc_file/paytm_loan_agreement.pdf +7739 -22
  26. api_chain_runner-1.0.0/example_chain.yaml +38 -0
  27. api_chain_runner-1.0.0/mobikwik.yaml +255 -0
  28. api_chain_runner-1.0.0/paytm.yaml +322 -0
  29. api_chain_runner-1.0.0/phonepe.yaml +300 -0
  30. api_chain_runner-1.0.0/phonepe_counter_offer.yaml +343 -0
  31. api_chain_runner-1.0.0/phonepe_udyam_reg.yaml +342 -0
  32. api_chain_runner-1.0.0/pyproject.toml +27 -0
  33. api_chain_runner-1.0.0/requirements.txt +7 -0
  34. api_chain_runner-1.0.0/setup.cfg +4 -0
  35. api_chain_runner-1.0.0/spice_money.yaml +249 -0
  36. api_chain_runner-1.0.0/tests/__init__.py +1 -0
  37. api_chain_runner-1.0.0/tests/test_executor.py +69 -0
  38. api_chain_runner-1.0.0/tests/test_models.py +195 -0
  39. api_chain_runner-1.0.0/tests/test_pause.py +103 -0
  40. api_chain_runner-1.0.0/tests/test_store.py +124 -0
@@ -0,0 +1,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ *.egg
9
+
10
+ # Virtual environment
11
+ .venv/
12
+ venv/
13
+ env/
14
+
15
+ # IDE
16
+ .idea/
17
+ .vscode/
18
+ .kiro/
19
+ *.swp
20
+ *.swo
21
+
22
+ # OS
23
+ .DS_Store
24
+ Thumbs.db
25
+
26
+ # pytest
27
+ .pytest_cache/
28
+
29
+ # Environment variables
30
+ .env
31
+
32
+ # Results/output
33
+ results.csv
@@ -0,0 +1,405 @@
1
+ # API Chain Runner - Architecture & Design Patterns
2
+
3
+ ## 1. HIGH-LEVEL DESIGN (HLD)
4
+
5
+ ### What is HLD?
6
+ HLD describes the **overall system structure** - how major components interact at a high level, without diving into implementation details. It answers: "What are the main parts and how do they talk to each other?"
7
+
8
+ ### System Architecture Diagram
9
+
10
+ ```
11
+ ┌─────────────────────────────────────────────────────────────┐
12
+ │ CLI Entry Point │
13
+ │ (__main__.py) │
14
+ │ - Parse arguments │
15
+ │ - Substitute environment variables │
16
+ │ - Create ChainRunner │
17
+ └────────────────────┬────────────────────────────────────────┘
18
+
19
+
20
+ ┌─────────────────────────────────────────────────────────────┐
21
+ │ ChainRunner (Orchestrator) │
22
+ │ - Loads YAML config │
23
+ │ - Initializes all components │
24
+ │ - Executes steps sequentially │
25
+ │ - Aggregates results │
26
+ └────────────────────┬────────────────────────────────────────┘
27
+
28
+ ┌────────────┼────────────┬──────────────┐
29
+ │ │ │ │
30
+ ▼ ▼ ▼ ▼
31
+ ┌────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐
32
+ │ Store │ │Resolver│ │Generator │ │ Executor │
33
+ │ │ │ │ │ │ │ │
34
+ │Saves & │ │Replaces│ │Generates │ │Makes HTTP│
35
+ │retrieves│ │${...} │ │unique │ │requests │
36
+ │responses│ │with │ │data │ │ │
37
+ │ │ │values │ │ │ │ │
38
+ └────────┘ └────────┘ └──────────┘ └──────────┘
39
+ │ │ │ │
40
+ └────────────┼────────────┴──────────────┘
41
+
42
+
43
+ ┌─────────────────┐
44
+ │ ResultLogger │
45
+ │ │
46
+ │ Logs all │
47
+ │ requests/ │
48
+ │ responses to │
49
+ │ CSV/XLSX │
50
+ └─────────────────┘
51
+ ```
52
+
53
+ ### Key Components at HLD Level
54
+
55
+ | Component | Purpose | Responsibility |
56
+ |-----------|---------|-----------------|
57
+ | **ChainRunner** | Orchestrator | Loads config, initializes components, runs steps |
58
+ | **ResponseStore** | Data Cache | Stores API responses for cross-step sharing |
59
+ | **ReferenceResolver** | Dynamic Substitution | Replaces `${step.key}` with actual values |
60
+ | **UniqueDataGenerator** | Data Generation | Creates unique test data |
61
+ | **StepExecutor** | Execution Engine | Makes HTTP requests, handles polling |
62
+ | **ResultLogger** | Output Handler | Writes results to CSV/XLSX |
63
+
64
+ ### Data Flow at HLD Level
65
+
66
+ ```
67
+ YAML Config
68
+
69
+ ChainRunner.load_chain() → Parse & validate
70
+
71
+ ChainRunner.run() → For each step:
72
+ ├─ ReferenceResolver.resolve() → Replace ${...}
73
+ ├─ UniqueDataGenerator.apply() → Inject unique data
74
+ ├─ StepExecutor.execute() → Make HTTP call
75
+ ├─ ResponseStore.save() → Store response
76
+ └─ ResultLogger.log() → Record details
77
+
78
+ ResultLogger.finalize() → Write CSV/XLSX
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 2. LOW-LEVEL DESIGN (LLD)
84
+
85
+ ### What is LLD?
86
+ LLD describes **detailed implementation** - how each component works internally, what algorithms are used, data structures, and specific logic.
87
+
88
+ ### Component-Level Details
89
+
90
+ #### **ChainRunner (runner.py)**
91
+ ```
92
+ Responsibilities:
93
+ - Load YAML file and parse into StepDefinition objects
94
+ - Create and initialize all dependencies (Store, Resolver, Generator, Executor, Logger)
95
+ - Execute steps sequentially
96
+ - Handle errors based on continue_on_error flag
97
+ - Return ChainResult with all step results
98
+
99
+ Key Methods:
100
+ __init__() → Initialize all components
101
+ load_chain() → Parse YAML, validate steps
102
+ _load_variables() → Pre-seed store with top-level variables
103
+ run() → Main execution loop
104
+ ```
105
+
106
+ #### **StepExecutor (executor.py)**
107
+ ```
108
+ Responsibilities:
109
+ - Execute a single API step
110
+ - Resolve references in URL, headers, payload
111
+ - Generate unique data for marked fields
112
+ - Make HTTP request with timeout (30 seconds)
113
+ - Handle polling (retry until expected value found)
114
+ - Store response in ResponseStore
115
+ - Catch and log errors
116
+
117
+ Key Methods:
118
+ execute() → Main entry point, handles polling logic
119
+ _execute_once() → Single HTTP request execution
120
+ _get_nested() → Extract nested values from response
121
+ ```
122
+
123
+ #### **ReferenceResolver (resolver.py)**
124
+ ```
125
+ Responsibilities:
126
+ - Find all ${...} patterns in strings
127
+ - Parse reference format: ${step_name.key_path}
128
+ - Retrieve values from ResponseStore using dot-notation
129
+ - Recursively resolve nested structures (dicts, lists)
130
+ - Preserve data types for full-string references
131
+ - Raise clear errors for missing references
132
+
133
+ Key Methods:
134
+ resolve() → Recursively resolve all references
135
+ find_references() → Extract ${...} patterns using regex
136
+ get_nested_value() → Traverse nested dicts with dot-notation
137
+ ```
138
+
139
+ #### **ResponseStore (store.py)**
140
+ ```
141
+ Responsibilities:
142
+ - Store API responses keyed by step name
143
+ - Retrieve values using dot-notation paths
144
+ - Support nested key access (e.g., "data.user.id")
145
+
146
+ Key Methods:
147
+ save() → Store response under step name
148
+ get() → Retrieve value using dot-notation
149
+ has() → Check if step response exists
150
+
151
+ Data Structure:
152
+ _data: dict[str, dict] = {
153
+ "auth": {"idToken": "abc123", "userId": 42},
154
+ "create_lead": {"leadId": "lead_001", "status": "PENDING"}
155
+ }
156
+ ```
157
+
158
+ #### **UniqueDataGenerator (generator.py)**
159
+ ```
160
+ Responsibilities:
161
+ - Generate unique emails (timestamp + UUID)
162
+ - Generate valid Indian PAN numbers
163
+ - Generate valid Indian mobile numbers
164
+ - Apply generators to specified payload fields
165
+
166
+ Key Methods:
167
+ generate_email() → Create unique email
168
+ generate_pan() → Create valid PAN (format: [A-Z]{3}P[A-Z][0-9]{4}[A-Z])
169
+ generate_mobile() → Create valid mobile (10 digits, 6-9 start)
170
+ apply() → Inject generated values into payload
171
+ ```
172
+
173
+ #### **ResultLogger (logger.py)**
174
+ ```
175
+ Responsibilities:
176
+ - Log every API call with full details
177
+ - Support CSV and XLSX output formats
178
+ - Create output directories if needed
179
+ - Write headers and data rows
180
+
181
+ Key Methods:
182
+ log() → Add a log entry
183
+ finalize() → Write all logs to file
184
+ _entry_to_row() → Convert LogEntry to CSV row
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 3. DESIGN PATTERNS USED
190
+
191
+ ### Pattern 1: **Dependency Injection (DI)**
192
+
193
+ **What it is:** Pass dependencies to a class instead of creating them inside.
194
+
195
+ **Where it's used:**
196
+ ```python
197
+ # In StepExecutor.__init__()
198
+ def __init__(
199
+ self,
200
+ resolver: ReferenceResolver, # Injected
201
+ generator: UniqueDataGenerator, # Injected
202
+ store: ResponseStore, # Injected
203
+ logger: ResultLogger, # Injected
204
+ ) -> None:
205
+ self.resolver = resolver
206
+ self.generator = generator
207
+ self.store = store
208
+ self.logger = logger
209
+ ```
210
+
211
+ **Why it's good:**
212
+ - Easy to test (pass mock objects)
213
+ - Loose coupling (components don't create each other)
214
+ - Flexible (can swap implementations)
215
+
216
+ ---
217
+
218
+ ### Pattern 2: **Strategy Pattern**
219
+
220
+ **What it is:** Different algorithms for the same task, selected at runtime.
221
+
222
+ **Where it's used:**
223
+ ```python
224
+ # In UniqueDataGenerator
225
+ unique_fields = {
226
+ "email": "email", # Strategy: generate email
227
+ "pan": "pan", # Strategy: generate PAN
228
+ "mobile": "mobile" # Strategy: generate mobile
229
+ }
230
+
231
+ # In ResultLogger
232
+ fmt = "csv" # Strategy: write CSV
233
+ fmt = "xlsx" # Strategy: write XLSX
234
+ ```
235
+
236
+ **Why it's good:**
237
+ - Easy to add new generator types
238
+ - Easy to add new output formats
239
+ - No if-else chains
240
+
241
+ ---
242
+
243
+ ### Pattern 3: **Template Method Pattern**
244
+
245
+ **What it is:** Define skeleton of algorithm, let subclasses fill in details.
246
+
247
+ **Where it's used:**
248
+ ```python
249
+ # In StepExecutor.execute()
250
+ def execute(self, step: StepDefinition) -> StepResult:
251
+ if not step.polling:
252
+ return self._execute_once(step) # Simple case
253
+
254
+ # Polling case: retry until condition met
255
+ while time.monotonic() - start_time < polling.max_timeout:
256
+ result = self._execute_once(step) # Template method
257
+ if result.response_data.get(polling.key_path) in polling.expected_values:
258
+ return result
259
+ time.sleep(polling.interval)
260
+ ```
261
+
262
+ **Why it's good:**
263
+ - Reusable logic for both simple and polling cases
264
+ - Clear separation of concerns
265
+
266
+ ---
267
+
268
+ ### Pattern 4: **Factory Pattern**
269
+
270
+ **What it is:** Create objects without specifying exact classes.
271
+
272
+ **Where it's used:**
273
+ ```python
274
+ # In ChainRunner.__init__()
275
+ self.store = ResponseStore()
276
+ self.resolver = ReferenceResolver(self.store)
277
+ self.generator = UniqueDataGenerator()
278
+ self.logger = ResultLogger(output_path)
279
+ self.executor = StepExecutor(
280
+ resolver=self.resolver,
281
+ generator=self.generator,
282
+ store=self.store,
283
+ logger=self.logger,
284
+ )
285
+ ```
286
+
287
+ **Why it's good:**
288
+ - Centralized object creation
289
+ - Easy to modify how objects are created
290
+ - Single place to initialize dependencies
291
+
292
+ ---
293
+
294
+ ### Pattern 5: **Decorator Pattern (Implicit)**
295
+
296
+ **What it is:** Add behavior to objects without modifying them.
297
+
298
+ **Where it's used:**
299
+ ```python
300
+ # In StepExecutor._execute_once()
301
+ # Original payload is "decorated" with:
302
+ # 1. Resolved references
303
+ # 2. Generated unique data
304
+ # 3. Logging wrapper
305
+
306
+ resolved_payload = self.resolver.resolve(step.payload)
307
+ unique_payload = self.generator.apply(resolved_payload, step.unique_fields)
308
+ # Then logged
309
+ ```
310
+
311
+ **Why it's good:**
312
+ - Payload is enhanced step-by-step
313
+ - Each enhancement is independent
314
+ - Original payload not modified
315
+
316
+ ---
317
+
318
+ ### Pattern 6: **Chain of Responsibility (Implicit)**
319
+
320
+ **What it is:** Pass request through chain of handlers.
321
+
322
+ **Where it's used:**
323
+ ```python
324
+ # In ChainRunner.run()
325
+ for step in self.steps:
326
+ result = self.executor.execute(step)
327
+ # Each step's output becomes input for next step
328
+ # Via ResponseStore
329
+ ```
330
+
331
+ **Why it's good:**
332
+ - Steps are loosely coupled
333
+ - Easy to add/remove steps
334
+ - Each step doesn't know about others
335
+
336
+ ---
337
+
338
+ ## 4. CODING ARCHITECTURE SUMMARY
339
+
340
+ ### Layered Architecture
341
+
342
+ ```
343
+ ┌─────────────────────────────────────────┐
344
+ │ Presentation Layer │
345
+ │ (__main__.py - CLI Interface) │
346
+ └─────────────────────────────────────────┘
347
+
348
+ ┌─────────────────────────────────────────┐
349
+ │ Orchestration Layer │
350
+ │ (ChainRunner - Coordinates execution) │
351
+ └─────────────────────────────────────────┘
352
+
353
+ ┌─────────────────────────────────────────┐
354
+ │ Business Logic Layer │
355
+ │ - StepExecutor (execution) │
356
+ │ - ReferenceResolver (substitution) │
357
+ │ - UniqueDataGenerator (data gen) │
358
+ │ - ResponseStore (data cache) │
359
+ └─────────────────────────────────────────┘
360
+
361
+ ┌─────────────────────────────────────────┐
362
+ │ Data Layer │
363
+ │ - ResultLogger (persistence) │
364
+ │ - Models (data contracts) │
365
+ └─────────────────────────────────────────┘
366
+ ```
367
+
368
+ ### Key Principles
369
+
370
+ | Principle | Implementation |
371
+ |-----------|-----------------|
372
+ | **Single Responsibility** | Each class has one job (Executor executes, Resolver resolves, etc.) |
373
+ | **Open/Closed** | Open for extension (add new generators), closed for modification |
374
+ | **Dependency Inversion** | Depend on abstractions (interfaces), not concrete classes |
375
+ | **DRY (Don't Repeat Yourself)** | Shared logic in ResponseStore, ReferenceResolver |
376
+ | **Separation of Concerns** | HTTP logic separate from data generation, logging, etc. |
377
+
378
+ ---
379
+
380
+ ## 5. QUICK REFERENCE TABLE
381
+
382
+ | Aspect | Details |
383
+ |--------|---------|
384
+ | **Architecture Type** | Layered + Component-based |
385
+ | **Design Patterns** | DI, Strategy, Template Method, Factory, Decorator, Chain of Responsibility |
386
+ | **Data Flow** | YAML → ChainRunner → StepExecutor → ResponseStore → ResultLogger |
387
+ | **Error Handling** | Try-catch with continue_on_error flag |
388
+ | **Extensibility** | Easy to add new generators, output formats, step types |
389
+ | **Testability** | High (DI makes mocking easy) |
390
+ | **Coupling** | Low (components communicate via interfaces) |
391
+ | **Cohesion** | High (each component has clear responsibility) |
392
+
393
+ ---
394
+
395
+ ## 6. WHEN TO USE EACH PATTERN (For Your Future Projects)
396
+
397
+ | Pattern | Use When |
398
+ |---------|----------|
399
+ | **Dependency Injection** | You want testable, loosely-coupled code |
400
+ | **Strategy** | You have multiple algorithms for same task |
401
+ | **Template Method** | You have similar algorithms with variations |
402
+ | **Factory** | You need centralized object creation |
403
+ | **Decorator** | You want to add behavior without modifying original |
404
+ | **Chain of Responsibility** | You have sequential processing with loose coupling |
405
+
@@ -0,0 +1,208 @@
1
+ # Why frozenset Instead of tuple or list?
2
+
3
+ ## Quick Answer
4
+ **frozenset** is used because:
5
+ 1. **Immutable** - Can't be accidentally modified
6
+ 2. **Fast lookups** - O(1) instead of O(n)
7
+ 3. **Semantic clarity** - Shows "this is a collection of valid values"
8
+ 4. **Prevents bugs** - Protects against accidental changes
9
+
10
+ ---
11
+
12
+ ## Detailed Comparison
13
+
14
+ ### 1. **Performance: Lookup Speed**
15
+
16
+ #### Using List
17
+ ```python
18
+ VALID_HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
19
+
20
+ # Checking if method is valid
21
+ if method in VALID_HTTP_METHODS: # O(n) - checks every item
22
+ pass
23
+ ```
24
+ - **Time Complexity**: O(n) - has to check each item
25
+ - For 7 items: checks up to 7 items
26
+ - For 1000 items: checks up to 1000 items
27
+
28
+ #### Using Tuple
29
+ ```python
30
+ VALID_HTTP_METHODS = ("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS")
31
+
32
+ if method in VALID_HTTP_METHODS: # O(n) - still checks every item
33
+ pass
34
+ ```
35
+ - **Time Complexity**: O(n) - same as list
36
+ - Slightly faster than list, but still slow for large collections
37
+
38
+ #### Using frozenset
39
+ ```python
40
+ VALID_HTTP_METHODS = frozenset({"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"})
41
+
42
+ if method in VALID_HTTP_METHODS: # O(1) - instant lookup
43
+ pass
44
+ ```
45
+ - **Time Complexity**: O(1) - instant, uses hash table
46
+ - Same speed whether 7 items or 7000 items
47
+
48
+ ### Real-World Impact
49
+ ```python
50
+ # If you validate 1 million API calls:
51
+ # List: 1,000,000 × 7 = 7,000,000 comparisons
52
+ # frozenset: 1,000,000 × 1 = 1,000,000 comparisons (7x faster!)
53
+ ```
54
+
55
+ ---
56
+
57
+ ### 2. **Immutability: Preventing Bugs**
58
+
59
+ #### Using List (DANGEROUS)
60
+ ```python
61
+ VALID_HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
62
+
63
+ # Somewhere in code, someone accidentally modifies it:
64
+ VALID_HTTP_METHODS.append("INVALID_METHOD") # ❌ BUG! Now validation is broken
65
+ VALID_HTTP_METHODS.pop() # ❌ BUG! Removed a valid method
66
+
67
+ # Now all validation is wrong for the entire application
68
+ ```
69
+
70
+ #### Using Tuple (SAFER)
71
+ ```python
72
+ VALID_HTTP_METHODS = ("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS")
73
+
74
+ VALID_HTTP_METHODS.append("INVALID_METHOD") # ❌ Error: tuple has no append
75
+ VALID_HTTP_METHODS[0] = "INVALID" # ❌ Error: tuple doesn't support item assignment
76
+ ```
77
+ - Prevents accidental modification
78
+ - But you can still do: `VALID_HTTP_METHODS = ["GET"]` (reassign the variable)
79
+
80
+ #### Using frozenset (SAFEST)
81
+ ```python
82
+ VALID_HTTP_METHODS = frozenset({"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"})
83
+
84
+ VALID_HTTP_METHODS.add("INVALID_METHOD") # ❌ Error: frozenset has no add
85
+ VALID_HTTP_METHODS.remove("GET") # ❌ Error: frozenset has no remove
86
+ VALID_HTTP_METHODS = ["GET"] # ❌ Error: can't reassign (if using const)
87
+ ```
88
+ - Completely immutable
89
+ - No way to modify it
90
+ - Protects the entire application
91
+
92
+ ---
93
+
94
+ ### 3. **Semantic Clarity: What Does It Mean?**
95
+
96
+ #### Using List
97
+ ```python
98
+ VALID_HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
99
+ # Unclear: Is order important? Can it be modified? Is it a queue?
100
+ ```
101
+
102
+ #### Using Tuple
103
+ ```python
104
+ VALID_HTTP_METHODS = ("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS")
105
+ # Clearer: Immutable, but order might matter
106
+ ```
107
+
108
+ #### Using frozenset
109
+ ```python
110
+ VALID_HTTP_METHODS = frozenset({"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"})
111
+ # Crystal clear: This is a fixed collection of valid values
112
+ # Order doesn't matter, can't be modified, fast lookups
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Comparison Table
118
+
119
+ | Feature | List | Tuple | frozenset |
120
+ |---------|------|-------|-----------|
121
+ | **Lookup Speed** | O(n) ❌ | O(n) ❌ | O(1) ✅ |
122
+ | **Immutable** | No ❌ | Yes ✅ | Yes ✅ |
123
+ | **Can be hashed** | No ❌ | Yes ✅ | Yes ✅ |
124
+ | **Order matters** | Yes | Yes | No |
125
+ | **Duplicates allowed** | Yes | Yes | No |
126
+ | **Memory efficient** | Medium | Good | Good |
127
+ | **Use for validation** | Bad | OK | Best ✅ |
128
+
129
+ ---
130
+
131
+ ## Real Code Example from Your Project
132
+
133
+ ```python
134
+ # In models.py - StepDefinition.validate()
135
+ def validate(self) -> None:
136
+ method_upper = self.method.upper() if self.method else ""
137
+ if method_upper not in VALID_HTTP_METHODS: # ← This lookup happens for EVERY step
138
+ raise ConfigurationError(...)
139
+
140
+ if self.unique_fields:
141
+ for field_path, gen_type in self.unique_fields.items():
142
+ if gen_type not in VALID_GENERATOR_TYPES: # ← This lookup happens for EVERY field
143
+ raise ConfigurationError(...)
144
+ ```
145
+
146
+ **Why frozenset matters here:**
147
+ - If you have 100 steps with 5 fields each = 500 lookups
148
+ - With list: 500 × 3 = 1500 comparisons
149
+ - With frozenset: 500 × 1 = 500 comparisons (3x faster!)
150
+
151
+ ---
152
+
153
+ ## When to Use Each
154
+
155
+ | Use Case | Best Choice |
156
+ |----------|-------------|
157
+ | **Fixed set of valid values** | frozenset ✅ |
158
+ | **Ordered sequence** | tuple or list |
159
+ | **Mutable collection** | list |
160
+ | **Performance-critical lookups** | frozenset ✅ |
161
+ | **Prevent accidental modification** | frozenset ✅ |
162
+ | **Use as dictionary key** | frozenset ✅ (tuple also works) |
163
+
164
+ ---
165
+
166
+ ## What If You Used List Instead?
167
+
168
+ ```python
169
+ # ❌ BAD - Using list
170
+ VALID_HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
171
+
172
+ # In validate() method - called 1000 times per run
173
+ if method_upper not in VALID_HTTP_METHODS: # O(n) lookup, 1000 times
174
+ raise ConfigurationError(...)
175
+
176
+ # Result: Slower validation, risk of accidental modification
177
+ ```
178
+
179
+ ```python
180
+ # ✅ GOOD - Using frozenset
181
+ VALID_HTTP_METHODS = frozenset({"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"})
182
+
183
+ # In validate() method - called 1000 times per run
184
+ if method_upper not in VALID_HTTP_METHODS: # O(1) lookup, 1000 times
185
+ raise ConfigurationError(...)
186
+
187
+ # Result: Fast validation, impossible to accidentally modify
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Key Takeaway
193
+
194
+ **frozenset is the professional choice for:**
195
+ - ✅ Constants that shouldn't change
196
+ - ✅ Validation collections
197
+ - ✅ Performance-critical lookups
198
+ - ✅ Preventing bugs from accidental modification
199
+
200
+ **Use list when:**
201
+ - You need to modify the collection
202
+ - Order matters
203
+ - You need duplicates
204
+
205
+ **Use tuple when:**
206
+ - You need immutability but order matters
207
+ - You want to use it as a dictionary key
208
+
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: api-chain-runner
3
+ Version: 1.0.0
4
+ Summary: Execute chained API calls defined in YAML configuration files
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: requests
8
+ Requires-Dist: pyyaml
9
+ Requires-Dist: openpyxl
10
+ Provides-Extra: test
11
+ Requires-Dist: hypothesis; extra == "test"
12
+ Requires-Dist: pytest; extra == "test"