Skip to content

Commit 43db43c

Browse files
authored
feat: add feature to save the openapi to disk (#158)
* feat: add feature to save the openapi to disk Signed-off-by: Derk Weijers <derk.weijers@alliander.com> * chore: add tests as well Signed-off-by: Derk Weijers <derk.weijers@alliander.com> * chore: cleanup Signed-off-by: Derk Weijers <derk.weijers@alliander.com> --------- Signed-off-by: Derk Weijers <derk.weijers@alliander.com>
1 parent 8257ca6 commit 43db43c

File tree

4 files changed

+264
-1
lines changed

4 files changed

+264
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,5 @@ Temporary Items
346346
/pytest-report.xml
347347
/.pycrunch-config.yaml
348348
/TESTING_GROUNDS.ipynb
349+
350+
openapi.json

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ wpla_clear_era5land = "weather_provider_api.scripts.erase_era5land_repository:ma
6363
wpla_clear_arome = "weather_provider_api.scripts.erase_arome_repository:main"
6464
wpla_clear_waarnemingen = "weather_provider_api.scripts.erase_waarnemingen_register:main"
6565
wpla_run_api = "weather_provider_api.main:main"
66+
generate-openapi = "weather_provider_api.scripts.openapi:generate_openapi_spec"
6667

6768
[[tool.poetry.source]]
6869
name = "PyPI"
@@ -109,4 +110,4 @@ ignore = [
109110
"tests/**" = ["S", "ANN"]
110111

111112
[tool.ruff.lint.pydocstyle]
112-
convention = "google"
113+
convention = "google"

tests/test_openapi.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
# SPDX-FileCopyrightText: 2019-2022 Alliander N.V.
5+
# SPDX-License-Identifier: MPL-2.0
6+
7+
import json
8+
from pathlib import Path
9+
from unittest.mock import MagicMock, Mock, mock_open, patch
10+
11+
import pytest
12+
13+
from weather_provider_api.scripts.openapi import generate_openapi_spec
14+
15+
16+
class TestGenerateOpenAPISpec:
17+
"""Test suite for the generate_openapi_spec function."""
18+
19+
def test_generate_openapi_spec_creates_file(self, tmp_path):
20+
"""Test that generate_openapi_spec creates an openapi.json file."""
21+
mock_spec = {
22+
"openapi": "3.0.2",
23+
"info": {"title": "Weather API (v2)", "version": "2.0.0"},
24+
"paths": {},
25+
}
26+
27+
with patch("weather_provider_api.scripts.openapi.v2_app") as mock_app:
28+
mock_app.openapi.return_value = mock_spec
29+
30+
with patch("builtins.open", mock_open()) as mock_file:
31+
generate_openapi_spec()
32+
33+
# Verify file was opened with correct parameters
34+
mock_file.assert_called_once_with("openapi.json", "w")
35+
36+
# Verify the spec was written to file
37+
handle = mock_file()
38+
written_content = "".join(
39+
call.args[0] for call in handle.write.call_args_list
40+
)
41+
assert written_content == json.dumps(mock_spec)
42+
43+
def test_generate_openapi_spec_calls_openapi_method(self):
44+
"""Test that the v2_app.openapi() method is called."""
45+
mock_spec = {"openapi": "3.0.2"}
46+
47+
with patch("weather_provider_api.scripts.openapi.v2_app") as mock_app:
48+
mock_app.openapi.return_value = mock_spec
49+
50+
with patch("builtins.open", mock_open()):
51+
generate_openapi_spec()
52+
53+
# Verify openapi() was called exactly once
54+
mock_app.openapi.assert_called_once_with()
55+
56+
def test_generate_openapi_spec_writes_valid_json(self):
57+
"""Test that the generated file contains valid JSON."""
58+
mock_spec = {
59+
"openapi": "3.0.2",
60+
"info": {
61+
"title": "Weather API (v2)",
62+
"version": "2.0.0",
63+
"description": "Test API",
64+
},
65+
"paths": {
66+
"/weather": {
67+
"get": {
68+
"summary": "Get weather data",
69+
"responses": {"200": {"description": "Success"}},
70+
}
71+
}
72+
},
73+
}
74+
75+
with patch("weather_provider_api.scripts.openapi.v2_app") as mock_app:
76+
mock_app.openapi.return_value = mock_spec
77+
78+
with patch("builtins.open", mock_open()) as mock_file:
79+
generate_openapi_spec()
80+
81+
handle = mock_file()
82+
written_content = "".join(
83+
call.args[0] for call in handle.write.call_args_list
84+
)
85+
86+
# Verify the content is valid JSON and matches the spec
87+
parsed_json = json.loads(written_content)
88+
assert parsed_json == mock_spec
89+
90+
def test_generate_openapi_spec_handles_complex_spec(self):
91+
"""Test that complex OpenAPI specifications are handled correctly."""
92+
mock_spec = {
93+
"openapi": "3.0.2",
94+
"info": {
95+
"title": "Weather API (v2)",
96+
"version": "2.0.0",
97+
"contact": {
98+
"name": "Test Team",
99+
"email": "test@example.com",
100+
},
101+
},
102+
"servers": [{"url": "https://api.example.com/api/v2"}],
103+
"paths": {
104+
"/weather/{location}": {
105+
"get": {
106+
"parameters": [
107+
{
108+
"name": "location",
109+
"in": "path",
110+
"required": True,
111+
"schema": {"type": "string"},
112+
}
113+
],
114+
"responses": {
115+
"200": {
116+
"description": "Successful response",
117+
"content": {
118+
"application/json": {
119+
"schema": {
120+
"type": "object",
121+
"properties": {
122+
"temperature": {"type": "number"},
123+
"humidity": {"type": "number"},
124+
},
125+
}
126+
}
127+
},
128+
}
129+
},
130+
}
131+
}
132+
},
133+
"components": {
134+
"schemas": {
135+
"WeatherData": {
136+
"type": "object",
137+
"properties": {
138+
"temperature": {"type": "number"},
139+
"humidity": {"type": "number"},
140+
},
141+
}
142+
}
143+
},
144+
}
145+
146+
with patch("weather_provider_api.scripts.openapi.v2_app") as mock_app:
147+
mock_app.openapi.return_value = mock_spec
148+
149+
with patch("builtins.open", mock_open()) as mock_file:
150+
generate_openapi_spec()
151+
152+
handle = mock_file()
153+
written_content = "".join(
154+
call.args[0] for call in handle.write.call_args_list
155+
)
156+
157+
parsed_json = json.loads(written_content)
158+
assert parsed_json == mock_spec
159+
assert "components" in parsed_json
160+
assert "schemas" in parsed_json["components"]
161+
162+
def test_generate_openapi_spec_overwrites_existing_file(self):
163+
"""Test that the function overwrites an existing openapi.json file."""
164+
mock_spec = {"openapi": "3.0.2", "info": {"title": "Test", "version": "1.0.0"}}
165+
166+
with patch("weather_provider_api.scripts.openapi.v2_app") as mock_app:
167+
mock_app.openapi.return_value = mock_spec
168+
169+
with patch("builtins.open", mock_open()) as mock_file:
170+
generate_openapi_spec()
171+
172+
# File should be opened in write mode, which overwrites
173+
mock_file.assert_called_once_with("openapi.json", "w")
174+
175+
def test_generate_openapi_spec_with_empty_paths(self):
176+
"""Test handling of OpenAPI spec with no paths defined."""
177+
mock_spec = {
178+
"openapi": "3.0.2",
179+
"info": {"title": "Weather API (v2)", "version": "2.0.0"},
180+
"paths": {},
181+
}
182+
183+
with patch("weather_provider_api.scripts.openapi.v2_app") as mock_app:
184+
mock_app.openapi.return_value = mock_spec
185+
186+
with patch("builtins.open", mock_open()) as mock_file:
187+
generate_openapi_spec()
188+
189+
handle = mock_file()
190+
written_content = "".join(
191+
call.args[0] for call in handle.write.call_args_list
192+
)
193+
194+
parsed_json = json.loads(written_content)
195+
assert parsed_json["paths"] == {}
196+
197+
def test_generate_openapi_spec_json_formatting(self):
198+
"""Test that the JSON is written in a compact format (no indentation)."""
199+
mock_spec = {
200+
"openapi": "3.0.2",
201+
"info": {"title": "Test", "version": "1.0.0"},
202+
}
203+
204+
with patch("weather_provider_api.scripts.openapi.v2_app") as mock_app:
205+
mock_app.openapi.return_value = mock_spec
206+
207+
with patch("builtins.open", mock_open()) as mock_file:
208+
generate_openapi_spec()
209+
210+
handle = mock_file()
211+
written_content = "".join(
212+
call.args[0] for call in handle.write.call_args_list
213+
)
214+
215+
# Verify it's compact JSON (no extra whitespace/newlines)
216+
expected = json.dumps(mock_spec)
217+
assert written_content == expected
218+
assert "\n" not in written_content # No pretty printing
219+
220+
def test_generate_openapi_spec_integration(self):
221+
"""Integration test verifying the complete flow."""
222+
mock_spec = {
223+
"openapi": "3.0.2",
224+
"info": {
225+
"title": "Weather API (v2)",
226+
"version": "2.73.16",
227+
},
228+
"paths": {
229+
"/weather/sources": {
230+
"get": {
231+
"summary": "Get available weather sources",
232+
}
233+
}
234+
},
235+
}
236+
237+
with patch("weather_provider_api.scripts.openapi.v2_app") as mock_app:
238+
mock_app.openapi.return_value = mock_spec
239+
240+
with patch("builtins.open", mock_open()) as mock_file:
241+
generate_openapi_spec()
242+
243+
# Verify the complete interaction
244+
mock_app.openapi.assert_called_once()
245+
mock_file.assert_called_once_with("openapi.json", "w")
246+
247+
handle = mock_file()
248+
handle.write.assert_called_once_with(json.dumps(mock_spec))
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import json
2+
3+
from weather_provider_api.core.application import WPLA_APPLICATION
4+
from weather_provider_api.versions.v2 import app as v2_app
5+
6+
7+
8+
def generate_openapi_spec():
9+
"""Generate OpenAPI specification and save it to a file."""
10+
spec = v2_app.openapi()
11+
with open("openapi.json", "w") as f:
12+
f.write(json.dumps(spec))

0 commit comments

Comments
 (0)