sefrone-api-e2e 1.0.0__py3-none-any.whl → 1.0.1__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.
- sefrone_api_e2e/api_e2e_manager.py +301 -85
- {sefrone_api_e2e-1.0.0.dist-info → sefrone_api_e2e-1.0.1.dist-info}/METADATA +2 -1
- sefrone_api_e2e-1.0.1.dist-info/RECORD +6 -0
- sefrone_api_e2e-1.0.0.dist-info/RECORD +0 -6
- {sefrone_api_e2e-1.0.0.dist-info → sefrone_api_e2e-1.0.1.dist-info}/WHEEL +0 -0
- {sefrone_api_e2e-1.0.0.dist-info → sefrone_api_e2e-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -3,31 +3,130 @@ import requests
|
|
|
3
3
|
import re
|
|
4
4
|
import os
|
|
5
5
|
import datetime
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List
|
|
6
9
|
|
|
7
10
|
class ApiE2ETestsManager:
|
|
11
|
+
# Precompiled patterns and defaults
|
|
12
|
+
VAR_PATTERN = re.compile(r"\$var\(([A-Za-z0-9_]+)\)")
|
|
13
|
+
STORED_PATTERN = re.compile(r"\{\$stored\.([a-zA-Z0-9_.]+)\}")
|
|
14
|
+
TEMPLATE_PATTERN = re.compile(r"\{\{\s*([^}]+?)\s*\}\}")
|
|
15
|
+
DEFAULT_TIMEOUT = 10 # seconds for HTTP requests
|
|
16
|
+
|
|
17
|
+
# Mutable state (reset at run start)
|
|
18
|
+
global_store: Dict[str, Any] = {}
|
|
19
|
+
scenario_results: List[Dict[str, Any]] = [] # holds timing + status info for all scenarios
|
|
20
|
+
|
|
8
21
|
@staticmethod
|
|
9
|
-
def run_all_tests(folder_path, is_verbose=False):
|
|
22
|
+
def run_all_tests(folder_path, env_file_path, is_verbose=False):
|
|
10
23
|
print("\nRunning API E2E tests...")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
|
|
25
|
+
# Reset shared state to avoid leakage between runs
|
|
26
|
+
ApiE2ETestsManager.global_store = {}
|
|
27
|
+
ApiE2ETestsManager.scenario_results = []
|
|
28
|
+
|
|
29
|
+
p = Path(folder_path)
|
|
30
|
+
scenario_files = sorted([f.name for f in p.iterdir() if f.is_file() and f.suffix == ".yaml"])
|
|
31
|
+
total_start = time.time()
|
|
32
|
+
|
|
33
|
+
for filename in scenario_files:
|
|
34
|
+
yaml_file_path = os.path.join(folder_path, filename)
|
|
35
|
+
scenario_name = os.path.splitext(filename)[0]
|
|
36
|
+
|
|
37
|
+
try:
|
|
14
38
|
print(f"\n--- Running scenario: {filename} ---")
|
|
15
|
-
ApiE2ETestsManager.
|
|
39
|
+
spec = ApiE2ETestsManager.parse_and_load_yaml(yaml_file_path, env_file_path)
|
|
40
|
+
start_time = time.time()
|
|
41
|
+
step_metrics = ApiE2ETestsManager.run_yaml_test(spec, scenario_name, is_verbose)
|
|
42
|
+
elapsed = time.time() - start_time
|
|
43
|
+
|
|
44
|
+
if all(step["result"] == "PASS" for step in step_metrics):
|
|
45
|
+
ApiE2ETestsManager.scenario_results.append({
|
|
46
|
+
"scenario": scenario_name,
|
|
47
|
+
"status": "[PASS]",
|
|
48
|
+
"steps": step_metrics,
|
|
49
|
+
"total_time": elapsed
|
|
50
|
+
})
|
|
51
|
+
else:
|
|
52
|
+
ApiE2ETestsManager.scenario_results.append({
|
|
53
|
+
"scenario": scenario_name,
|
|
54
|
+
"status": "[FAIL]",
|
|
55
|
+
"steps": step_metrics,
|
|
56
|
+
"total_time": elapsed
|
|
57
|
+
})
|
|
58
|
+
# mark remaining scenarios as skipped
|
|
59
|
+
remaining = scenario_files[scenario_files.index(filename)+1:]
|
|
60
|
+
for skipped_file in remaining:
|
|
61
|
+
skipped_name = os.path.splitext(skipped_file)[0]
|
|
62
|
+
ApiE2ETestsManager.scenario_results.append({
|
|
63
|
+
"scenario": skipped_name,
|
|
64
|
+
"status": "[SKIP]",
|
|
65
|
+
"steps": [],
|
|
66
|
+
"total_time": 0
|
|
67
|
+
})
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
except AssertionError:
|
|
71
|
+
# mark the failed one
|
|
72
|
+
failed_elapsed = time.time() - start_time if 'start_time' in locals() else 0
|
|
73
|
+
ApiE2ETestsManager.scenario_results.append({
|
|
74
|
+
"scenario": scenario_name,
|
|
75
|
+
"status": "[FAIL]",
|
|
76
|
+
"steps": step_metrics if 'step_metrics' in locals() else [],
|
|
77
|
+
"total_time": failed_elapsed
|
|
78
|
+
})
|
|
79
|
+
# mark remaining scenarios as skipped
|
|
80
|
+
remaining = scenario_files[scenario_files.index(filename)+1:]
|
|
81
|
+
for skipped_file in remaining:
|
|
82
|
+
skipped_name = os.path.splitext(skipped_file)[0]
|
|
83
|
+
ApiE2ETestsManager.scenario_results.append({
|
|
84
|
+
"scenario": skipped_name,
|
|
85
|
+
"status": "[SKIP]",
|
|
86
|
+
"steps": [],
|
|
87
|
+
"total_time": 0
|
|
88
|
+
})
|
|
89
|
+
break # stop all further execution
|
|
90
|
+
|
|
91
|
+
total_elapsed = time.time() - total_start
|
|
16
92
|
print("\nAPI E2E tests completed.")
|
|
93
|
+
ApiE2ETestsManager.print_summary(total_elapsed)
|
|
17
94
|
|
|
18
95
|
@staticmethod
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
96
|
+
def read_env_value_from_file(env_file_path, env_var):
|
|
97
|
+
try:
|
|
98
|
+
with open(env_file_path, 'r', encoding='utf-8') as file:
|
|
99
|
+
for raw in file:
|
|
100
|
+
line = raw.strip()
|
|
101
|
+
if not line or line.startswith('#'):
|
|
102
|
+
continue
|
|
103
|
+
if line.startswith(f'{env_var}='):
|
|
104
|
+
return line[len(f'{env_var}='):].strip()
|
|
105
|
+
except FileNotFoundError:
|
|
106
|
+
print(f"[WARN] Environment file not found: {env_file_path}")
|
|
107
|
+
return ""
|
|
108
|
+
print(f"[WARN] Environment variable '{env_var}' not found in {env_file_path}.")
|
|
109
|
+
return ""
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def parse_and_load_yaml(yaml_file_path, env_file_path):
|
|
113
|
+
with open(yaml_file_path, "r", encoding="utf-8") as f:
|
|
114
|
+
content = f.read()
|
|
115
|
+
|
|
116
|
+
def replacer(match):
|
|
117
|
+
key = match.group(1)
|
|
118
|
+
return ApiE2ETestsManager.read_env_value_from_file(env_file_path, key)
|
|
119
|
+
|
|
120
|
+
resolved_content = ApiE2ETestsManager.VAR_PATTERN.sub(replacer, content)
|
|
121
|
+
return yaml.safe_load(resolved_content)
|
|
22
122
|
|
|
23
123
|
@staticmethod
|
|
24
124
|
def check_type(value, expected_type):
|
|
25
|
-
"""Check if value matches expected type keyword."""
|
|
26
125
|
type_map = {
|
|
27
126
|
"string": str,
|
|
28
127
|
"number": (int, float),
|
|
29
128
|
"boolean": bool,
|
|
30
|
-
"datetime": str
|
|
129
|
+
"datetime": str
|
|
31
130
|
}
|
|
32
131
|
if expected_type not in type_map:
|
|
33
132
|
raise ValueError(f"Unknown type in YAML: {expected_type}")
|
|
@@ -46,7 +145,6 @@ class ApiE2ETestsManager:
|
|
|
46
145
|
|
|
47
146
|
@staticmethod
|
|
48
147
|
def validate_structure(data, expected):
|
|
49
|
-
"""Recursively validate structure & type expectations."""
|
|
50
148
|
if isinstance(expected, dict):
|
|
51
149
|
if not isinstance(data, dict):
|
|
52
150
|
raise AssertionError(f"Expected dict but got {type(data)}")
|
|
@@ -60,7 +158,7 @@ class ApiE2ETestsManager:
|
|
|
60
158
|
if len(expected) > 0:
|
|
61
159
|
for item in data:
|
|
62
160
|
ApiE2ETestsManager.validate_structure(item, expected[0])
|
|
63
|
-
elif isinstance(expected, str):
|
|
161
|
+
elif isinstance(expected, str):
|
|
64
162
|
if not ApiE2ETestsManager.check_type(data, expected):
|
|
65
163
|
raise AssertionError(f"Expected {expected}, got {data} ({type(data)})")
|
|
66
164
|
else:
|
|
@@ -80,89 +178,207 @@ class ApiE2ETestsManager:
|
|
|
80
178
|
|
|
81
179
|
def replacer(match):
|
|
82
180
|
expr = match.group(1).strip()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
raise KeyError(f"Unknown variable: {expr}")
|
|
97
|
-
|
|
98
|
-
return re.sub(r"\{\{\s*([^}]+?)\s*\}\}", replacer, template_str)
|
|
181
|
+
val = resolve_path(expr, context)
|
|
182
|
+
if isinstance(val, str):
|
|
183
|
+
return repr(val) if add_quotes else str(val)
|
|
184
|
+
elif val is True:
|
|
185
|
+
return "True"
|
|
186
|
+
elif val is False:
|
|
187
|
+
return "False"
|
|
188
|
+
elif val is None:
|
|
189
|
+
return "None"
|
|
190
|
+
else:
|
|
191
|
+
return str(val)
|
|
192
|
+
|
|
193
|
+
return ApiE2ETestsManager.TEMPLATE_PATTERN.sub(replacer, template_str)
|
|
99
194
|
|
|
100
195
|
@staticmethod
|
|
101
|
-
def substitute_stored(
|
|
102
|
-
"""Substitute {$stored.key} placeholders."""
|
|
196
|
+
def substitute_stored(expr, scenario_name):
|
|
103
197
|
def repl(match):
|
|
104
|
-
key = match.group(1)
|
|
105
|
-
|
|
106
|
-
|
|
198
|
+
key = match.group(1).strip()
|
|
199
|
+
parts = key.split(".")
|
|
200
|
+
if len(parts) == 1:
|
|
201
|
+
subkey = parts[0]
|
|
202
|
+
return str(ApiE2ETestsManager.global_store.get(f"{scenario_name}.{subkey}", f"<MISSING:{subkey}>"))
|
|
203
|
+
elif len(parts) == 2:
|
|
204
|
+
scen, subkey = parts
|
|
205
|
+
return str(ApiE2ETestsManager.global_store.get(f"{scen}.{subkey}", f"<MISSING:{key}>"))
|
|
206
|
+
else:
|
|
207
|
+
raise ValueError(f"Invalid stored key reference: {key} (Too many dots)")
|
|
208
|
+
|
|
209
|
+
return ApiE2ETestsManager.STORED_PATTERN.sub(repl, expr)
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def store_value(key, value, scenario_name):
|
|
213
|
+
namespaced_key = f"{scenario_name}.{key}"
|
|
214
|
+
ApiE2ETestsManager.global_store[namespaced_key] = value
|
|
215
|
+
print(f" Saved: {namespaced_key} = {value}")
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def try_eval(rendered, expr):
|
|
219
|
+
# Basic supported operators
|
|
220
|
+
operators = [">=", "<=", ">", "<", "==", "!=", "in", "not in", "include"]
|
|
221
|
+
|
|
222
|
+
matched_op = None
|
|
223
|
+
for op in operators:
|
|
224
|
+
if f" {op} " in rendered:
|
|
225
|
+
matched_op = op
|
|
226
|
+
left, right = [part.strip() for part in rendered.split(f" {op} ", 1)]
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
if not matched_op:
|
|
230
|
+
raise AssertionError(f"Unsupported assertion format: '{expr}'")
|
|
231
|
+
|
|
232
|
+
# Try to evaluate left and right sides
|
|
233
|
+
def try_eval_part(value):
|
|
234
|
+
# Try to interpret as int/float/bool/None/string
|
|
235
|
+
if re.match(r"^-?\d+\.\d+$", value):
|
|
236
|
+
return float(value)
|
|
237
|
+
elif re.match(r"^-?\d+$", value):
|
|
238
|
+
return int(value)
|
|
239
|
+
elif value.lower() == "true":
|
|
240
|
+
return True
|
|
241
|
+
elif value.lower() == "false":
|
|
242
|
+
return False
|
|
243
|
+
elif value.lower() == "none":
|
|
244
|
+
return None
|
|
245
|
+
elif value.startswith('"') and value.endswith('"'):
|
|
246
|
+
return value.strip('"')
|
|
247
|
+
elif value.startswith("'") and value.endswith("'"):
|
|
248
|
+
return value.strip("'")
|
|
249
|
+
else:
|
|
250
|
+
return value # treat as literal string
|
|
251
|
+
|
|
252
|
+
left_val = try_eval_part(left)
|
|
253
|
+
right_val = try_eval_part(right)
|
|
254
|
+
|
|
255
|
+
# Comparison logic
|
|
256
|
+
result = False
|
|
257
|
+
if matched_op == "==":
|
|
258
|
+
result = left_val == right_val
|
|
259
|
+
elif matched_op == "!=":
|
|
260
|
+
result = left_val != right_val
|
|
261
|
+
elif matched_op == ">":
|
|
262
|
+
result = left_val > right_val
|
|
263
|
+
elif matched_op == "<":
|
|
264
|
+
result = left_val < right_val
|
|
265
|
+
elif matched_op == ">=":
|
|
266
|
+
result = left_val >= right_val
|
|
267
|
+
elif matched_op == "<=":
|
|
268
|
+
result = left_val <= right_val
|
|
269
|
+
elif matched_op == "in":
|
|
270
|
+
result = left_val in right_val
|
|
271
|
+
elif matched_op == "not in":
|
|
272
|
+
result = left_val not in right_val
|
|
273
|
+
elif matched_op == "include":
|
|
274
|
+
result = right_val in left_val
|
|
275
|
+
|
|
276
|
+
if not result:
|
|
277
|
+
raise AssertionError(
|
|
278
|
+
f"Assertion failed: {expr} → evaluated as {left_val} {matched_op} {right_val}"
|
|
279
|
+
)
|
|
280
|
+
return True
|
|
107
281
|
|
|
108
282
|
@staticmethod
|
|
109
|
-
def run_yaml_test(
|
|
110
|
-
spec = ApiE2ETestsManager.load_yaml(path)
|
|
283
|
+
def run_yaml_test(spec, scenario_name, is_verbose=False):
|
|
111
284
|
base_url = spec.get("base_url", "")
|
|
112
285
|
steps = spec.get("steps", [])
|
|
113
|
-
|
|
286
|
+
step_metrics = []
|
|
114
287
|
|
|
115
|
-
print(f"Running test: {spec
|
|
288
|
+
print(f"Running test: {spec.get('name', scenario_name)}")
|
|
116
289
|
print("=" * 60)
|
|
117
|
-
|
|
290
|
+
# use a session for connection pooling and to set a default timeout
|
|
291
|
+
session = requests.Session()
|
|
118
292
|
for step in steps:
|
|
119
|
-
print(f"Step: {step['name']}")
|
|
120
|
-
url = base_url + ApiE2ETestsManager.substitute_stored(step['endpoint'], stored)
|
|
121
|
-
method = step['method'].upper()
|
|
122
|
-
body = step.get("body")
|
|
123
|
-
expect = step.get("expect", {})
|
|
124
|
-
|
|
125
|
-
# Make HTTP request
|
|
126
|
-
resp = requests.request(method, url, json=body)
|
|
127
|
-
print(f" → {method} {url} -> {resp.status_code}")
|
|
128
|
-
|
|
129
|
-
# Validate status code
|
|
130
|
-
expected_status = expect.get("status")
|
|
131
|
-
if expected_status and resp.status_code != expected_status:
|
|
132
|
-
if is_verbose:
|
|
133
|
-
print(f"Response body: {resp.text}")
|
|
134
|
-
raise AssertionError(f"Expected {expected_status}, got {resp.status_code}")
|
|
135
|
-
|
|
136
|
-
# Parse response JSON
|
|
137
293
|
try:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
print(f" Saved: {key} = {rendered}")
|
|
154
|
-
|
|
155
|
-
# Assertions
|
|
156
|
-
if "assertions" in step:
|
|
157
|
-
for expr in step["assertions"]:
|
|
158
|
-
rendered = ApiE2ETestsManager.render_template(expr, {"body": resp_json, "stored": stored}, add_quotes=True)
|
|
159
|
-
try:
|
|
160
|
-
if not eval(rendered):
|
|
161
|
-
raise AssertionError(f"Assertion failed: {expr}, rendered as '{rendered}'")
|
|
162
|
-
except Exception as e:
|
|
163
|
-
raise AssertionError(f"Assertion error in '{expr}', rendered as '{rendered}': {e}")
|
|
164
|
-
|
|
165
|
-
print(" ✅ Step passed.\n")
|
|
294
|
+
step_start = time.time()
|
|
295
|
+
print(f"Step: {step['name']}")
|
|
296
|
+
url = base_url + ApiE2ETestsManager.substitute_stored(step['endpoint'], scenario_name)
|
|
297
|
+
method = step['method'].upper()
|
|
298
|
+
|
|
299
|
+
request_headers = {}
|
|
300
|
+
if "request_headers" in step:
|
|
301
|
+
for header in step["request_headers"]:
|
|
302
|
+
for header_key, header_value in header.items():
|
|
303
|
+
if isinstance(header_value, str):
|
|
304
|
+
header_value = ApiE2ETestsManager.substitute_stored(header_value, scenario_name)
|
|
305
|
+
request_headers[header_key] = header_value
|
|
306
|
+
|
|
307
|
+
body = step.get("body")
|
|
308
|
+
expect = step.get("expect", {})
|
|
166
309
|
|
|
310
|
+
resp = session.request(method, url, json=body, headers=request_headers, timeout=ApiE2ETestsManager.DEFAULT_TIMEOUT)
|
|
311
|
+
print(f" -> {method} {url} -> {resp.status_code}")
|
|
312
|
+
|
|
313
|
+
expected_status = expect.get("status")
|
|
314
|
+
if expected_status and resp.status_code != expected_status:
|
|
315
|
+
raise AssertionError(f"Expected {expected_status}, got {resp.status_code}")
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
resp_json = resp.json()
|
|
319
|
+
except Exception:
|
|
320
|
+
raise AssertionError("Response is not valid JSON")
|
|
321
|
+
|
|
322
|
+
if "body" in expect:
|
|
323
|
+
if is_verbose:
|
|
324
|
+
print(f"Response body: {resp_json}")
|
|
325
|
+
ApiE2ETestsManager.validate_structure(resp_json, expect["body"])
|
|
326
|
+
|
|
327
|
+
if "save" in step:
|
|
328
|
+
for key, path_expr in step["save"].items():
|
|
329
|
+
rendered = ApiE2ETestsManager.render_template(path_expr, {"body": resp_json})
|
|
330
|
+
ApiE2ETestsManager.store_value(key, rendered, scenario_name)
|
|
331
|
+
|
|
332
|
+
if "assertions" in step:
|
|
333
|
+
for expr in step["assertions"]:
|
|
334
|
+
rendered = ApiE2ETestsManager.render_template(expr, {"body": resp_json}, add_quotes=True)
|
|
335
|
+
ApiE2ETestsManager.try_eval(rendered, expr)
|
|
336
|
+
|
|
337
|
+
elapsed = time.time() - step_start
|
|
338
|
+
step_metrics.append({"name": step["name"], "time": elapsed, "result": "PASS"})
|
|
339
|
+
print(f"[OK ] Step passed in {elapsed:.2f}s\n")
|
|
340
|
+
except Exception as e:
|
|
341
|
+
failed_elapsed = time.time() - step_start
|
|
342
|
+
step_metrics.append({"name": step["name"], "time": failed_elapsed, "result": "FAIL", "error": str(e)})
|
|
343
|
+
print(f"[FAIL] Step failed in {failed_elapsed:.2f}s error: {str(e)}\n")
|
|
344
|
+
break
|
|
167
345
|
print("=" * 60)
|
|
168
|
-
|
|
346
|
+
return step_metrics
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def print_summary(total_time):
|
|
350
|
+
print("\n[SUMMARY] TEST RESULTS")
|
|
351
|
+
print("=" * 80)
|
|
352
|
+
|
|
353
|
+
headers = ["Scenario", "Status", "Time (s)", "Steps"]
|
|
354
|
+
col_widths = [30, 7, 8, 50]
|
|
355
|
+
|
|
356
|
+
header_line = f"{headers[0]:<{col_widths[0]}} | {headers[1]:<{col_widths[1]}} | {headers[2]:<{col_widths[2]}} | {headers[3]}"
|
|
357
|
+
print(header_line)
|
|
358
|
+
print("-" * len(header_line))
|
|
359
|
+
|
|
360
|
+
for result in ApiE2ETestsManager.scenario_results:
|
|
361
|
+
print(f"{result['scenario']:<{col_widths[0]}} | "
|
|
362
|
+
f"{result['status']:<{col_widths[1]}} | "
|
|
363
|
+
f"{result['total_time']:<{col_widths[2]}.2f} | ")
|
|
364
|
+
|
|
365
|
+
if result["steps"]:
|
|
366
|
+
for step in result["steps"]:
|
|
367
|
+
parsed = f"[{step['result']}] {step['name']} ({step['time']:.2f}s)"
|
|
368
|
+
print(f"{'':<{col_widths[0]}} | "
|
|
369
|
+
f"{'':<{col_widths[1]}} | "
|
|
370
|
+
f"{'':<{col_widths[2]}} | "
|
|
371
|
+
f"{parsed:<{col_widths[3]}}")
|
|
372
|
+
if step['result'] == "FAIL" and 'error' in step:
|
|
373
|
+
print(f"{'':<{col_widths[0]}} | "
|
|
374
|
+
f"{'':<{col_widths[1]}} | "
|
|
375
|
+
f"{'':<{col_widths[2]}} | "
|
|
376
|
+
f"===> {step['error']}")
|
|
377
|
+
else:
|
|
378
|
+
print(f"{'':<{col_widths[0]}} | "
|
|
379
|
+
f"{'':<{col_widths[1]}} | "
|
|
380
|
+
f"{'':<{col_widths[2]}} | "
|
|
381
|
+
f"{'(no steps run)':<{col_widths[3]}}")
|
|
382
|
+
|
|
383
|
+
print("=" * 80)
|
|
384
|
+
print(f"Total Execution Time: {total_time:.2f}s\n")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sefrone-api-e2e
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: A Python package to provide e2e testing helpers for sefrone API projects
|
|
5
5
|
Home-page: https://bitbucket.org/sefrone/sefrone_pypi
|
|
6
6
|
Author: Sefrone
|
|
@@ -13,6 +13,7 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.7
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
Requires-Dist: PyYAML>=6.0.1
|
|
16
|
+
Requires-Dist: requests
|
|
16
17
|
|
|
17
18
|
This is not a usable Python package, but the name is reserved by SARL Sefrone.
|
|
18
19
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
sefrone_api_e2e/__init__.py,sha256=NNUHigTCBkjdwCfQXVEA7urxF1-A8HF1_IuWJ8LuO-k,91
|
|
2
|
+
sefrone_api_e2e/api_e2e_manager.py,sha256=3oA9JqDQR9L_Fj5YRgeXpPcFxl28FRLNN_hdjB1017E,16464
|
|
3
|
+
sefrone_api_e2e-1.0.1.dist-info/METADATA,sha256=-qjd_wb2tI9jnSQKe7rt0fIMe4Is7-012z8SGutmlCw,721
|
|
4
|
+
sefrone_api_e2e-1.0.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
5
|
+
sefrone_api_e2e-1.0.1.dist-info/top_level.txt,sha256=19WO3CsUWUiGtZBotT587N-tkxxjctKOPDEHWpHpS8M,16
|
|
6
|
+
sefrone_api_e2e-1.0.1.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
sefrone_api_e2e/__init__.py,sha256=NNUHigTCBkjdwCfQXVEA7urxF1-A8HF1_IuWJ8LuO-k,91
|
|
2
|
-
sefrone_api_e2e/api_e2e_manager.py,sha256=3LDA-kIZLyhVTvxZFE4huJdVTKQfsZmIdiTpeGwoVPs,6984
|
|
3
|
-
sefrone_api_e2e-1.0.0.dist-info/METADATA,sha256=k4CtrauVJStV0vCz3tb5hkNM552nz5KyMro1wG7AK4U,696
|
|
4
|
-
sefrone_api_e2e-1.0.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
5
|
-
sefrone_api_e2e-1.0.0.dist-info/top_level.txt,sha256=19WO3CsUWUiGtZBotT587N-tkxxjctKOPDEHWpHpS8M,16
|
|
6
|
-
sefrone_api_e2e-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|