Skip to content

Commit 56fc28a

Browse files
An app for getting general info and stats about the API edpoints and their 'examples' state.
1 parent 3a2c22b commit 56fc28a

File tree

6 files changed

+270
-0
lines changed

6 files changed

+270
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import os
2+
import re
3+
from typing import List
4+
from pathlib import Path
5+
from constants import EXAMPLES_FOLDER
6+
from dataclasses import dataclass, field
7+
8+
@dataclass
9+
class EndpointInfo:
10+
path: str = None
11+
has_examples_subfolder: bool = False
12+
num_request_examples: int = 0
13+
num_response_examples: int = 0
14+
num_examples: int = 0
15+
examples_subfolders = []
16+
recognized_examples_subfolders: set[str] = field(default_factory=set)
17+
examples_response_codes: set[str] = field(default_factory=set)
18+
19+
class EndpointInfoGenerator:
20+
def __init__(self):
21+
self.spec_path = "."
22+
23+
# Get all the folders in a path
24+
def get_folders(self, path: str) -> List[Path]:
25+
return [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))]
26+
27+
def get_files(self, path: str) -> List[str]:
28+
return [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
29+
30+
def is_example_file(self, file: str) -> bool:
31+
if file.endswith(".yaml"):
32+
return True
33+
print(f"WARNING: Found non-YAML example file: {file}")
34+
return False
35+
36+
def get_example_files_in_folder(self, path: str) -> List[str]:
37+
example_files = []
38+
for file in self.get_files(path):
39+
if self.is_example_file(file):
40+
example_files.append(file)
41+
return example_files
42+
43+
def get_request_subfolder(self, endpoint_path) -> str:
44+
examples_path = os.path.join(endpoint_path, EXAMPLES_FOLDER)
45+
request_examples_path = os.path.join(examples_path, "request")
46+
if os.path.exists(request_examples_path):
47+
return "request"
48+
return None
49+
50+
def get_response_subfolders(self, endpoint_path) -> int:
51+
examples_path = os.path.join(endpoint_path, EXAMPLES_FOLDER)
52+
response_examples_folders = []
53+
response_examples_path = os.path.join(examples_path, "response")
54+
if os.path.exists(response_examples_path):
55+
response_examples_folders.append("response")
56+
else:
57+
examples_subfolders = self.get_folders(examples_path)
58+
for examples_subfolder in examples_subfolders:
59+
# Look for folders of the pattern "nnn_response"
60+
if re.match(r"[0-9]{3}_response", examples_subfolder):
61+
response_examples_folders.append(examples_subfolder)
62+
return response_examples_folders
63+
64+
def get_response_code_from_response_folder(self, folder: str) -> str:
65+
if folder == "response":
66+
return "200"
67+
match = re.match(r"(\d{3})_response", folder)
68+
if match:
69+
return match.group(1)
70+
raise Exception(f"Invalid response folder: {folder}")
71+
72+
def get_endpoint_info(self, endpoint_path: str) -> EndpointInfo:
73+
endpoint_path_relative_to_spec = os.path.relpath(endpoint_path, self.spec_path)
74+
endpoint_info = EndpointInfo(path=endpoint_path_relative_to_spec)
75+
# If there is no 'examples' folder, return EndpointInfo with
76+
# default values
77+
examples_path = os.path.join(endpoint_path, EXAMPLES_FOLDER)
78+
if not os.path.exists(examples_path):
79+
return endpoint_info
80+
endpoint_info.examples_subfolders = self.get_folders(examples_path)
81+
request_subfolder = self.get_request_subfolder(endpoint_path)
82+
if request_subfolder:
83+
endpoint_info.recognized_examples_subfolders.add(request_subfolder)
84+
examples_request_path = os.path.join(examples_path, request_subfolder)
85+
endpoint_info.num_request_examples = len(self.get_example_files_in_folder(examples_request_path))
86+
response_subfolders = self.get_response_subfolders(endpoint_path)
87+
if (len(response_subfolders) > 0):
88+
endpoint_info.recognized_examples_subfolders.update(response_subfolders)
89+
for response_subfolder in response_subfolders:
90+
response_code = self.get_response_code_from_response_folder(response_subfolder)
91+
endpoint_info.examples_response_codes.add(response_code)
92+
examples_response_path = os.path.join(examples_path, response_subfolder)
93+
endpoint_info.num_response_examples += len(self.get_example_files_in_folder(examples_response_path))
94+
endpoint_info.num_examples = endpoint_info.num_request_examples + endpoint_info.num_response_examples
95+
return endpoint_info
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import os
2+
from typing import List
3+
from pathlib import Path
4+
5+
class EndpointPathsFinder:
6+
def __init__(self):
7+
self.spec_path = "."
8+
9+
def get_folders(self, path: str) -> List[Path]:
10+
return [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))]
11+
12+
def is_endpoint_group_folder(self, folder: str) -> bool:
13+
# Other than _global, any folder starting with underscore is
14+
# not an endpoint group folder
15+
if folder == "_global":
16+
return True
17+
if folder.startswith("_"):
18+
return False
19+
return True
20+
21+
def get_endpoint_group_folders(self) -> List[Path]:
22+
folders = self.get_folders(self.spec_path)
23+
return [f for f in folders if self.is_endpoint_group_folder(f)]
24+
25+
def is_endpoint_folder(self, folder: str) -> bool:
26+
if folder == "_types":
27+
return False
28+
return True
29+
30+
def get_endpoint_folders(self, path: str) -> List[Path]:
31+
folders = self.get_folders(path)
32+
return [f for f in folders if self.is_endpoint_folder(f)]
33+
34+
def get_endpoint_paths(self) -> List[str]:
35+
endpoint_paths = []
36+
group_folders = self.get_endpoint_group_folders()
37+
for group_folder in group_folders:
38+
group_path = os.path.join(self.spec_path, group_folder)
39+
group_endpoint_folders = self.get_endpoint_folders(group_path)
40+
for endpoint_folder in group_endpoint_folders:
41+
endpoint_paths.append(os.path.join(group_path, endpoint_folder))
42+
return endpoint_paths
43+
44+
def find_paths(self) -> List[str]:
45+
return self.get_endpoint_paths()
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import List
2+
from dataclasses import dataclass
3+
from EndpointPathsFinder import EndpointPathsFinder
4+
from EndpointInfoGenerator import EndpointInfo, EndpointInfoGenerator
5+
from ExamplesStatsGenerator import ExampleStats, ExamplesStatsGenerator
6+
7+
@dataclass
8+
class ExamplesInfo:
9+
endpoints_info: List[EndpointInfo]
10+
stats: ExampleStats
11+
12+
class ExamplesInfoGenerator:
13+
def __init__(self):
14+
self.endpoint_paths_finder = EndpointPathsFinder()
15+
self.endpoint_info_processor = EndpointInfoGenerator()
16+
17+
def get_examples_info(self) -> ExamplesInfo:
18+
endpoint_paths_finder = EndpointPathsFinder()
19+
endpoint_paths = endpoint_paths_finder.find_paths()
20+
endpoint_info_list = []
21+
stats = ExampleStats()
22+
for endpoint_path in endpoint_paths:
23+
stats.num_endpoints += 1
24+
endpoint_info = self.endpoint_info_processor.get_endpoint_info(endpoint_path)
25+
endpoint_info_list.append(endpoint_info)
26+
27+
examples_stats_generator = ExamplesStatsGenerator(endpoint_info_list)
28+
stats = examples_stats_generator.get_stats()
29+
30+
examples_info = ExamplesInfo(endpoint_info_list, stats)
31+
return examples_info
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from dataclasses import dataclass
2+
from typing import List
3+
from EndpointInfoGenerator import EndpointInfo
4+
5+
@dataclass
6+
class ExampleStats:
7+
num_endpoints: int = 0
8+
num_endpoints_with_examples: int = 0
9+
num_endpoints_with_no_examples: int = 0
10+
num_endpoints_with_only_request_examples: int = 0
11+
num_endpoints_with_only_response_examples: int = 0
12+
num_endpoints_with_both_request_and_response_examples: int = 0
13+
num_endpoints_with_non_200_response_code_examples: int = 0
14+
max_examples_per_endpoint: int = 0
15+
16+
class ExamplesStatsGenerator:
17+
def __init__(self, endpoint_info_list: List[EndpointInfo]):
18+
self.endpoint_info_list = endpoint_info_list
19+
self.spec_path = "."
20+
21+
def get_stats(self) -> ExampleStats:
22+
stats = ExampleStats()
23+
for endpoint_info in self.endpoint_info_list:
24+
stats.num_endpoints += 1
25+
26+
if endpoint_info.num_examples > 0:
27+
stats.num_endpoints_with_examples += 1
28+
29+
if endpoint_info.num_examples > stats.max_examples_per_endpoint:
30+
stats.max_examples_per_endpoint = endpoint_info.num_examples
31+
32+
if endpoint_info.num_request_examples > 0 and endpoint_info.num_response_examples == 0:
33+
stats.num_endpoints_with_only_request_examples += 1
34+
elif endpoint_info.num_request_examples == 0 and endpoint_info.num_response_examples > 0:
35+
stats.num_endpoints_with_only_response_examples += 1
36+
elif endpoint_info.num_request_examples > 0 and endpoint_info.num_response_examples > 0:
37+
stats.num_endpoints_with_both_request_and_response_examples += 1
38+
else:
39+
stats.num_endpoints_with_no_examples += 1
40+
41+
non_200_response_codes = endpoint_info.examples_response_codes - {"200"}
42+
if len(non_200_response_codes) > 0:
43+
stats.num_endpoints_with_non_200_response_code_examples += 1
44+
return stats
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DEFAULT_SPEC_PATH = "../../../specification"
2+
EXAMPLES_FOLDER = "examples"

src/scripts/examples-stats/main.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/python3
2+
3+
import os
4+
from constants import DEFAULT_SPEC_PATH
5+
from EndpointPathsFinder import EndpointPathsFinder
6+
from ExamplesInfoGenerator import ExamplesInfoGenerator, ExamplesInfo, ExampleStats
7+
8+
def print_stats(stats: ExampleStats):
9+
print ("===============")
10+
print ("==== Stats ====")
11+
print ("===============")
12+
print(f"Number of endpoints: {stats.num_endpoints}")
13+
print(f"Number of endpoints with no request or response examples: {stats.num_endpoints_with_no_examples}")
14+
print(f"Number of endpoints with examples: {stats.num_endpoints_with_examples}")
15+
print(f" {stats.num_endpoints_with_only_request_examples:>4}: Only request examples")
16+
print(f" {stats.num_endpoints_with_only_response_examples:>4}: Only response examples")
17+
print(f" {stats.num_endpoints_with_both_request_and_response_examples:>4}: Both request and response examples")
18+
print(f"Number of endpoints with non-200 response code examples: {stats.num_endpoints_with_non_200_response_code_examples}")
19+
print(f"Max examples per endpoint: {stats.max_examples_per_endpoint}")
20+
print ("===============\n")
21+
22+
def main():
23+
# Using a default spc path. We should add an option
24+
# for getting the spec path as a command line argument
25+
os.chdir(DEFAULT_SPEC_PATH)
26+
examples_info_generator = ExamplesInfoGenerator()
27+
examples_info: ExamplesInfo = examples_info_generator.get_examples_info()
28+
# === print stats
29+
print_stats(examples_info.stats)
30+
# === print paths with max examples
31+
print("Paths with max examples:")
32+
for endpoint_info in examples_info.endpoints_info:
33+
if endpoint_info.num_examples == examples_info.stats.max_examples_per_endpoint:
34+
print(f" {endpoint_info.path} with {endpoint_info.num_examples} examples")
35+
print()
36+
# === print all recognized examples subfolders
37+
all_examples_subfolders = set()
38+
all_recognized_examples_subfolders = set()
39+
for endpoint_info in examples_info.endpoints_info:
40+
all_examples_subfolders.update(endpoint_info.examples_subfolders)
41+
all_recognized_examples_subfolders.update(endpoint_info.recognized_examples_subfolders)
42+
print("All recognized subfolders of 'examples' folder:")
43+
for folder in all_recognized_examples_subfolders:
44+
print(f" {folder}")
45+
print()
46+
# === print unrecognized examples subfolders
47+
unrecognized_examples_subfolders = all_examples_subfolders - all_recognized_examples_subfolders
48+
print("unrecognized subfolders of 'examples' folder:")
49+
for folder in unrecognized_examples_subfolders:
50+
print(f" {folder}")
51+
52+
if __name__ == "__main__":
53+
main()

0 commit comments

Comments
 (0)