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.
- api_chain_runner-1.0.0/.gitignore +33 -0
- api_chain_runner-1.0.0/ARCHITECTURE.md +405 -0
- api_chain_runner-1.0.0/FROZENSET_EXPLANATION.md +208 -0
- api_chain_runner-1.0.0/PKG-INFO +12 -0
- api_chain_runner-1.0.0/README.md +386 -0
- api_chain_runner-1.0.0/api_chain_runner/__init__.py +18 -0
- api_chain_runner-1.0.0/api_chain_runner/__main__.py +138 -0
- api_chain_runner-1.0.0/api_chain_runner/executor.py +285 -0
- api_chain_runner-1.0.0/api_chain_runner/generator.py +126 -0
- api_chain_runner-1.0.0/api_chain_runner/logger.py +89 -0
- api_chain_runner-1.0.0/api_chain_runner/models.py +149 -0
- api_chain_runner-1.0.0/api_chain_runner/pause.py +104 -0
- api_chain_runner-1.0.0/api_chain_runner/resolver.py +118 -0
- api_chain_runner-1.0.0/api_chain_runner/runner.py +416 -0
- api_chain_runner-1.0.0/api_chain_runner/store.py +80 -0
- api_chain_runner-1.0.0/api_chain_runner.egg-info/PKG-INFO +12 -0
- api_chain_runner-1.0.0/api_chain_runner.egg-info/SOURCES.txt +38 -0
- api_chain_runner-1.0.0/api_chain_runner.egg-info/dependency_links.txt +1 -0
- api_chain_runner-1.0.0/api_chain_runner.egg-info/entry_points.txt +2 -0
- api_chain_runner-1.0.0/api_chain_runner.egg-info/requires.txt +7 -0
- api_chain_runner-1.0.0/api_chain_runner.egg-info/top_level.txt +1 -0
- api_chain_runner-1.0.0/bharatpe.yaml +285 -0
- api_chain_runner-1.0.0/doc_file/SELFIE.jpeg +0 -0
- api_chain_runner-1.0.0/doc_file/aadhaar.xml +39 -0
- api_chain_runner-1.0.0/doc_file/paytm_loan_agreement.pdf +7739 -22
- api_chain_runner-1.0.0/example_chain.yaml +38 -0
- api_chain_runner-1.0.0/mobikwik.yaml +255 -0
- api_chain_runner-1.0.0/paytm.yaml +322 -0
- api_chain_runner-1.0.0/phonepe.yaml +300 -0
- api_chain_runner-1.0.0/phonepe_counter_offer.yaml +343 -0
- api_chain_runner-1.0.0/phonepe_udyam_reg.yaml +342 -0
- api_chain_runner-1.0.0/pyproject.toml +27 -0
- api_chain_runner-1.0.0/requirements.txt +7 -0
- api_chain_runner-1.0.0/setup.cfg +4 -0
- api_chain_runner-1.0.0/spice_money.yaml +249 -0
- api_chain_runner-1.0.0/tests/__init__.py +1 -0
- api_chain_runner-1.0.0/tests/test_executor.py +69 -0
- api_chain_runner-1.0.0/tests/test_models.py +195 -0
- api_chain_runner-1.0.0/tests/test_pause.py +103 -0
- 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"
|