Skip to content

Commit ee6a947

Browse files
Initial commit of openapi-analyzer, a tool for getting some info and stats about the endpoints defined in an OpenAPI spec.
1 parent 4e28f92 commit ee6a947

File tree

8 files changed

+261
-0
lines changed

8 files changed

+261
-0
lines changed

src/scripts/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import os
2+
from dataclasses import dataclass, field
3+
from EndpointInfo import EndpointInfo
4+
from constants import ENDPOINT_OPERATIONS
5+
from openapi_pydantic.v3.v3_0 import OpenAPI, Reference
6+
7+
8+
@dataclass
9+
class RequestStats:
10+
num_operations: int = 0
11+
num_with_body: int = 0
12+
num_with_examples: int = 0
13+
num_examples: int = 0
14+
content_types: set[str] = field(default_factory=set)
15+
16+
@dataclass
17+
class ResponseCodeStats:
18+
num_operations: int = 0
19+
num_with_body: int = 0
20+
num_with_examples: int = 0
21+
num_examples: int = 0
22+
content_types: set[str] = field(default_factory=set)
23+
24+
@dataclass
25+
class ResponseStats:
26+
num_operations: int = 0
27+
28+
@dataclass
29+
class OperationStats:
30+
request_stats: RequestStats = field(default_factory=RequestStats)
31+
response_stats: dict[str, ResponseCodeStats] = field(default_factory=dict)
32+
33+
@dataclass
34+
class DetailedStats:
35+
num_endpoints: int = 0
36+
num_operations: int = 0
37+
operation_stats: dict[str, OperationStats] = field(default_factory=dict)
38+
39+
40+
class DetailedStatsGenerator:
41+
def __init__(self, openapi_spec: OpenAPI):
42+
self.openapi_spec = openapi_spec
43+
44+
def get_endpoint_info_list(self) -> list[EndpointInfo]:
45+
endpoint_info_list = []
46+
for path, path_item in self.openapi_spec.paths.items():
47+
endpointInfo = EndpointInfo.from_path(path, path_item)
48+
endpoint_info_list.append(endpointInfo)
49+
return endpoint_info_list
50+
51+
def get_detailed_stats(self) -> DetailedStats:
52+
endpoint_info_list = self.get_endpoint_info_list()
53+
stats = DetailedStats()
54+
stats.num_endpoints = len(endpoint_info_list)
55+
for operation in ENDPOINT_OPERATIONS:
56+
for endpoint_info in endpoint_info_list:
57+
if operation in endpoint_info.operations:
58+
stats.num_operations += 1
59+
if operation not in stats.operation_stats:
60+
stats.operation_stats[operation] = OperationStats()
61+
operation_stats = stats.operation_stats[operation]
62+
operation_stats.request_stats.num_operations += 1
63+
if endpoint_info.operations[operation].requestBody:
64+
requestBody = endpoint_info.operations[operation].requestBody
65+
if isinstance(requestBody, Reference):
66+
component_request_ref = os.path.basename(requestBody.ref)
67+
requestBody = self.openapi_spec.components.requestBodies[component_request_ref]
68+
for content_type, media_type in requestBody.content.items():
69+
operation_stats.request_stats.content_types.add(content_type)
70+
if media_type.examples:
71+
operation_stats.request_stats.num_with_examples += 1
72+
operation_stats.request_stats.num_examples += len(media_type.examples)
73+
operation_stats.request_stats.num_with_body += 1
74+
if endpoint_info.operations[operation].responses:
75+
for response_code, response in endpoint_info.operations[operation].responses.items():
76+
if response_code not in operation_stats.response_stats:
77+
operation_stats.response_stats[response_code] = ResponseCodeStats()
78+
operation_stats.response_stats[response_code].num_operations += 1
79+
if isinstance(response, Reference):
80+
component_response_ref = os.path.basename(response.ref)
81+
response = self.openapi_spec.components.responses[component_response_ref]
82+
if response.content:
83+
for content_type, media_type in response.content.items():
84+
operation_stats.response_stats[response_code].content_types.add(content_type)
85+
if media_type.examples:
86+
operation_stats.response_stats[response_code].num_with_examples += 1
87+
operation_stats.response_stats[response_code].num_examples += len(media_type.examples)
88+
operation_stats.response_stats[response_code].num_with_body += 1
89+
return stats
90+
91+
def print_detailed_stats(self, stats: DetailedStats):
92+
print("========================")
93+
print("==== Detailed Stats ====")
94+
print("========================")
95+
print(f"Number of endpoints: {stats.num_endpoints}")
96+
print(f"Number of operations: {stats.num_operations}")
97+
for operation, operation_stats in stats.operation_stats.items():
98+
print(f" {operation}: {operation_stats.request_stats.num_operations}")
99+
print(" Requests:")
100+
print(f" with body: {operation_stats.request_stats.num_with_body}")
101+
print(" Content types:")
102+
for content_type in operation_stats.request_stats.content_types:
103+
print(f" {content_type}")
104+
print(f" Has examples: {operation_stats.request_stats.num_with_examples}")
105+
print(f" Number of examples: {operation_stats.request_stats.num_examples}")
106+
for response_code, response_code_stats in operation_stats.response_stats.items():
107+
print(" Responses:")
108+
print(f" '{response_code}': {response_code_stats.num_operations}")
109+
print(f" Has content: {response_code_stats.num_with_body}")
110+
print(" Content types:")
111+
for content_type in response_code_stats.content_types:
112+
print(f" {content_type}")
113+
print(f" Has examples: {response_code_stats.num_with_examples}")
114+
print(f" Number of examples: {response_code_stats.num_examples}")
115+
print()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from openapi_pydantic.v3.v3_0 import PathItem
2+
from constants import ENDPOINT_OPERATIONS
3+
4+
class EndpointInfo:
5+
def __init__(self, path: str):
6+
self.path = path
7+
self.operations = {}
8+
self.summary = None
9+
self.description = None
10+
self.parameters = []
11+
12+
def init(self, path_item: PathItem):
13+
self.summary = path_item.summary
14+
self.description = path_item.description
15+
self.parameters = path_item.parameters
16+
for operation in ENDPOINT_OPERATIONS:
17+
if getattr(path_item, operation):
18+
self.operations[operation] = getattr(path_item, operation)
19+
20+
@staticmethod
21+
def from_path(path: str, path_item: PathItem):
22+
endpoint_info = EndpointInfo(path)
23+
endpoint_info.init(path_item)
24+
return endpoint_info
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from DetailedStatsGenerator import DetailedStatsGenerator
2+
from SummaryStatsGenerator import SummaryStatsGenerator
3+
from openapi_pydantic.v3.v3_0 import OpenAPI
4+
5+
6+
class OpenapiAnalyzer:
7+
def __init__(self, openapi_filepath):
8+
self.openapi_filepath = openapi_filepath
9+
10+
def run(self):
11+
openapi_spec = OpenAPI.parse_file(self.openapi_filepath)
12+
print(f"OpenAPI version: {openapi_spec.openapi}\n")
13+
detailed_stats_generator = DetailedStatsGenerator(openapi_spec)
14+
detailed_stats = detailed_stats_generator.get_detailed_stats()
15+
summary_stats_generator = SummaryStatsGenerator(detailed_stats)
16+
summary_stats = summary_stats_generator.get_summary_stats()
17+
summary_stats_generator.print_summary_stats(summary_stats)
18+
detailed_stats_generator.print_detailed_stats(detailed_stats)
19+
20+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from dataclasses import dataclass, field
2+
from DetailedStatsGenerator import DetailedStats
3+
4+
5+
@dataclass
6+
class SummaryStats:
7+
num_endpoints: int = 0
8+
num_operations: int = 0
9+
num_requests_with_body: int = 0
10+
num_requests_with_examples: int = 0
11+
num_request_examples: int = 0
12+
num_responses_with_body: int = 0
13+
num_responses_with_examples: int = 0
14+
num_response_examples: int = 0
15+
num_examples: int = 0
16+
request_content_types: set[str] = field(default_factory=set)
17+
response_content_types: set[str] = field(default_factory=set)
18+
19+
20+
class SummaryStatsGenerator:
21+
def __init__(self, detailed_stats: DetailedStats):
22+
self.detailed_stats = detailed_stats
23+
24+
def get_summary_stats(self) -> SummaryStats:
25+
summary_stats = SummaryStats()
26+
summary_stats.num_endpoints = self.detailed_stats.num_endpoints
27+
summary_stats.num_operations = self.detailed_stats.num_operations
28+
for _operation, operation_stats in self.detailed_stats.operation_stats.items():
29+
summary_stats.num_requests_with_body += operation_stats.request_stats.num_with_body
30+
summary_stats.num_requests_with_examples += operation_stats.request_stats.num_with_examples
31+
summary_stats.num_request_examples += operation_stats.request_stats.num_examples
32+
summary_stats.request_content_types.update(operation_stats.request_stats.content_types)
33+
for _response_code, response_code_stats in operation_stats.response_stats.items():
34+
summary_stats.num_responses_with_body += response_code_stats.num_with_body
35+
summary_stats.num_responses_with_examples += response_code_stats.num_with_examples
36+
summary_stats.num_response_examples += response_code_stats.num_examples
37+
summary_stats.response_content_types.update(response_code_stats.content_types)
38+
return summary_stats
39+
40+
def print_summary_stats(self, stats: SummaryStats):
41+
print("=======================")
42+
print("==== Summary Stats ====")
43+
print("=======================")
44+
print(f"Number of endpoints: {stats.num_endpoints}")
45+
print(f"Number of operations: {stats.num_operations}")
46+
print(" Requests:")
47+
print(f" with body: {stats.num_requests_with_body}")
48+
print(f" with examples: {stats.num_requests_with_examples}")
49+
print(f" number of examples: {stats.num_request_examples}")
50+
print(" Content types:")
51+
for content_type in stats.request_content_types:
52+
print(f" {content_type}")
53+
print(" Responses:")
54+
print(f" with body: {stats.num_responses_with_body}")
55+
print(f' with examples: {stats.num_responses_with_examples}')
56+
print(f" number of examples: {stats.num_response_examples}")
57+
print(" Content types:")
58+
for content_type in stats.response_content_types:
59+
print(f" {content_type}")
60+
print()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
DEFAULT_OUTPUT_PATH = "../../../output"
2+
DEFAULT_OPENAPI_FOLDER = "openapi"
3+
DEFAULT_OPENAPI_FILE = "elasticsearch-openapi.json"
4+
5+
ENDPOINT_OPERATIONS = [
6+
"get",
7+
"put",
8+
"post",
9+
"delete",
10+
"options",
11+
"head",
12+
"patch",
13+
"trace"
14+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env python
2+
3+
import os
4+
from constants import (
5+
DEFAULT_OUTPUT_PATH,
6+
DEFAULT_OPENAPI_FOLDER,
7+
DEFAULT_OPENAPI_FILE
8+
)
9+
from OpenapiAnalyzer import OpenapiAnalyzer
10+
11+
def main():
12+
openpi_filepath = os.path.join(DEFAULT_OUTPUT_PATH,
13+
DEFAULT_OPENAPI_FOLDER,
14+
DEFAULT_OPENAPI_FILE)
15+
if not os.path.exists(openpi_filepath):
16+
print(f"OpenAPI file not found: {openpi_filepath}")
17+
return
18+
openapi_analyzer = OpenapiAnalyzer(openpi_filepath)
19+
openapi_analyzer.run()
20+
21+
if __name__ == "__main__":
22+
main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
openapi-pydantic==0.5.1

0 commit comments

Comments
 (0)