mooring-data-generator 0.11.0__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.
- mooring_data_generator/__init__.py +0 -0
- mooring_data_generator/builder.py +340 -0
- mooring_data_generator/cli.py +56 -0
- mooring_data_generator/file_worker.py +75 -0
- mooring_data_generator/http_worker.py +69 -0
- mooring_data_generator/models.py +73 -0
- mooring_data_generator/openapi.py +52 -0
- mooring_data_generator/receiver.py +185 -0
- mooring_data_generator-0.11.0.dist-info/METADATA +178 -0
- mooring_data_generator-0.11.0.dist-info/RECORD +12 -0
- mooring_data_generator-0.11.0.dist-info/WHEEL +4 -0
- mooring_data_generator-0.11.0.dist-info/entry_points.txt +4 -0
|
File without changes
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import random
|
|
4
|
+
from math import ceil
|
|
5
|
+
|
|
6
|
+
from .models import BentData, BerthData, HookData, PortData, RadarData, ShipData
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
# A list of well-known Western Australian port names
|
|
11
|
+
WA_PORT_NAMES: list[str] = [
|
|
12
|
+
"Port Hedland",
|
|
13
|
+
"Dampier",
|
|
14
|
+
"Fremantle",
|
|
15
|
+
"Kwinana",
|
|
16
|
+
"Bunbury",
|
|
17
|
+
"Esperance",
|
|
18
|
+
"Albany",
|
|
19
|
+
"Geraldton",
|
|
20
|
+
"Broome",
|
|
21
|
+
"Wyndham",
|
|
22
|
+
"Derby",
|
|
23
|
+
"Carnarvon",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
NAUTICAL_SUPERLATIVES: list[str] = [
|
|
28
|
+
"Majestic",
|
|
29
|
+
"Sovereign",
|
|
30
|
+
"Resolute",
|
|
31
|
+
"Valiant",
|
|
32
|
+
"Vigilant",
|
|
33
|
+
"Dauntless",
|
|
34
|
+
"Liberty",
|
|
35
|
+
"Enduring",
|
|
36
|
+
"Gallant",
|
|
37
|
+
"Noble",
|
|
38
|
+
"Guardian",
|
|
39
|
+
"Intrepid",
|
|
40
|
+
"Courageous",
|
|
41
|
+
"Steadfast",
|
|
42
|
+
"Regal",
|
|
43
|
+
"Stalwart",
|
|
44
|
+
"Indomitable",
|
|
45
|
+
"Invincible",
|
|
46
|
+
"Triumphant",
|
|
47
|
+
"Victorious",
|
|
48
|
+
"Glorious",
|
|
49
|
+
"Fearless",
|
|
50
|
+
"Mighty",
|
|
51
|
+
"Bold",
|
|
52
|
+
"Brave",
|
|
53
|
+
"Formidable",
|
|
54
|
+
"Relentless",
|
|
55
|
+
"Valorous",
|
|
56
|
+
"Audacious",
|
|
57
|
+
"Diligent",
|
|
58
|
+
"Implacable",
|
|
59
|
+
"Indefatigable",
|
|
60
|
+
"Prosperous",
|
|
61
|
+
"Seaborne",
|
|
62
|
+
"Seagoing",
|
|
63
|
+
"Oceanic",
|
|
64
|
+
"Maritime",
|
|
65
|
+
"Coastal",
|
|
66
|
+
"Pelagic",
|
|
67
|
+
"Windward",
|
|
68
|
+
"Leeward",
|
|
69
|
+
"Tempestuous",
|
|
70
|
+
"Sturdy",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
NAUTICAL_BASE_NAMES: list[str] = [
|
|
74
|
+
# Western
|
|
75
|
+
"Amelia",
|
|
76
|
+
"Charlotte",
|
|
77
|
+
"Olivia",
|
|
78
|
+
"Sophia",
|
|
79
|
+
"Emily",
|
|
80
|
+
"Grace",
|
|
81
|
+
# East Asian
|
|
82
|
+
"Hana",
|
|
83
|
+
"Mei",
|
|
84
|
+
"Yuna",
|
|
85
|
+
"Sakura",
|
|
86
|
+
"Aiko",
|
|
87
|
+
"Keiko",
|
|
88
|
+
# South Asian
|
|
89
|
+
"Asha",
|
|
90
|
+
"Priya",
|
|
91
|
+
"Anika",
|
|
92
|
+
"Riya",
|
|
93
|
+
"Sana",
|
|
94
|
+
"Neha",
|
|
95
|
+
# Southeast Asian
|
|
96
|
+
"Linh",
|
|
97
|
+
"Thao",
|
|
98
|
+
"Trang",
|
|
99
|
+
"Ngoc",
|
|
100
|
+
"Anh",
|
|
101
|
+
"Nicha",
|
|
102
|
+
# Latin (Spanish/Portuguese/LatAm)
|
|
103
|
+
"Camila",
|
|
104
|
+
"Valentina",
|
|
105
|
+
"Isabela",
|
|
106
|
+
"Gabriela",
|
|
107
|
+
"Lucia",
|
|
108
|
+
"Paula",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
BENT_NAMES: list[str] = [f"BNT{x:03d}" for x in range(1, 999)]
|
|
113
|
+
|
|
114
|
+
SHIP_IDS: list[str] = [f"{x:04d}" for x in range(1, 9999)]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
MEAN_TENSIONS = 6
|
|
118
|
+
STDEV_TENSIONS = 5
|
|
119
|
+
MEAN_DISTANCES = 9.38
|
|
120
|
+
STDEV_DISTANCES = 6.73
|
|
121
|
+
MEAN_CHANGES = 0.68
|
|
122
|
+
STDEV_CHANGES = 2.6
|
|
123
|
+
|
|
124
|
+
BENT_COUNT_MIN = 9
|
|
125
|
+
BENT_COUNT_MAX = 15
|
|
126
|
+
|
|
127
|
+
HOOK_COUNT_MULTIPLIER = 3
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def random_single_use_choice(list_of_strings: list[str]) -> str:
|
|
131
|
+
"""Source a one-time random string from a list of strings"""
|
|
132
|
+
random_str = random.choice(list_of_strings)
|
|
133
|
+
list_of_strings.remove(random_str)
|
|
134
|
+
return random_str
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def random_ship_name() -> str:
|
|
138
|
+
"""Generate a random ship name by combining a nautical superlative with a potential ship name.
|
|
139
|
+
|
|
140
|
+
The format will be "<Superlative> <Name>". Example: "Majestic Amelia" or "Valiant Sophia".
|
|
141
|
+
"""
|
|
142
|
+
global NAUTICAL_SUPERLATIVES
|
|
143
|
+
global NAUTICAL_BASE_NAMES
|
|
144
|
+
return f"{random_single_use_choice(NAUTICAL_SUPERLATIVES)} {random_single_use_choice(NAUTICAL_BASE_NAMES)}"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def random_wa_port_name() -> str:
|
|
148
|
+
"""Return a random Western Australian port name.
|
|
149
|
+
Preventing the option from being selected in the future."""
|
|
150
|
+
global WA_PORT_NAMES
|
|
151
|
+
return random_single_use_choice(WA_PORT_NAMES)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def random_bent_name() -> str:
|
|
155
|
+
"""Return a random bent name."""
|
|
156
|
+
global BENT_NAMES
|
|
157
|
+
return random_single_use_choice(BENT_NAMES)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def generate_ship() -> ShipData:
|
|
161
|
+
"""Generate a ship data instance with unique random name and unique id"""
|
|
162
|
+
global SHIP_IDS
|
|
163
|
+
return ShipData(
|
|
164
|
+
name=random_ship_name(),
|
|
165
|
+
vessel_id=random_single_use_choice(SHIP_IDS),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class HookWorker:
|
|
170
|
+
"""a worker class for generating and managing changes in Hook data."""
|
|
171
|
+
|
|
172
|
+
def __init__(self, hook_number: int, attached_line: str):
|
|
173
|
+
self.name: str = f"Hook {hook_number}"
|
|
174
|
+
self.active: bool = random.choice([True, False, False])
|
|
175
|
+
# a 5% change of being in fault state
|
|
176
|
+
self.fault: bool = random.choices([True, False], weights=[0.05, 0.95])[0]
|
|
177
|
+
self.attached_line = None
|
|
178
|
+
self.tension = None
|
|
179
|
+
if self.active:
|
|
180
|
+
self.attached_line = attached_line
|
|
181
|
+
self.update()
|
|
182
|
+
|
|
183
|
+
def update(self):
|
|
184
|
+
if self.active:
|
|
185
|
+
self.tension = abs(ceil(random.gauss(MEAN_CHANGES, STDEV_CHANGES)))
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def data(self) -> HookData:
|
|
189
|
+
# noinspection PyTypeChecker
|
|
190
|
+
return HookData(
|
|
191
|
+
name=self.name,
|
|
192
|
+
tension=self.tension,
|
|
193
|
+
faulted=self.fault,
|
|
194
|
+
attached_line=self.attached_line,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class BentWorker:
|
|
199
|
+
"""a worker class for managing bents and cascading data"""
|
|
200
|
+
|
|
201
|
+
def __init__(self, bent_number: int, total_bents: int):
|
|
202
|
+
self.bent_number: int = bent_number
|
|
203
|
+
self.name = random_bent_name()
|
|
204
|
+
self.hooks: list[HookWorker] = []
|
|
205
|
+
bent_position = bent_number / total_bents
|
|
206
|
+
if bent_position < 0.2:
|
|
207
|
+
attached_line = "HEAD"
|
|
208
|
+
elif 0.8 < bent_position:
|
|
209
|
+
attached_line = "STERN"
|
|
210
|
+
elif 0.4 < bent_position < 0.6:
|
|
211
|
+
attached_line = "BREAST"
|
|
212
|
+
else:
|
|
213
|
+
attached_line = "SPRING"
|
|
214
|
+
hook_count_start: int = (
|
|
215
|
+
(self.bent_number * HOOK_COUNT_MULTIPLIER) - HOOK_COUNT_MULTIPLIER + 1
|
|
216
|
+
)
|
|
217
|
+
for hook_number in range(hook_count_start, hook_count_start + HOOK_COUNT_MULTIPLIER):
|
|
218
|
+
self.hooks.append(HookWorker(hook_number, attached_line=attached_line))
|
|
219
|
+
|
|
220
|
+
def update(self):
|
|
221
|
+
"""update the bent and cascading data"""
|
|
222
|
+
for hook in self.hooks:
|
|
223
|
+
hook.update()
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def data(self) -> BentData:
|
|
227
|
+
return BentData(
|
|
228
|
+
name=self.name,
|
|
229
|
+
hooks=[hook.data for hook in self.hooks],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class RadarWorker:
|
|
234
|
+
"""a worker class for generating and managing changes in Radar data."""
|
|
235
|
+
|
|
236
|
+
def __init__(self, name: str):
|
|
237
|
+
self.name: str = name
|
|
238
|
+
self.active: bool = random.choice([True, False, False])
|
|
239
|
+
self.distance: float | None = None
|
|
240
|
+
self.change: float | None = None
|
|
241
|
+
if self.active:
|
|
242
|
+
self.distance: float = abs(random.gauss(MEAN_DISTANCES, STDEV_DISTANCES))
|
|
243
|
+
self.change: float = abs(random.gauss(MEAN_CHANGES, STDEV_CHANGES))
|
|
244
|
+
|
|
245
|
+
def update(self) -> tuple[float, float]:
|
|
246
|
+
if self.active:
|
|
247
|
+
new_distance: float = abs(random.gauss(MEAN_TENSIONS, STDEV_TENSIONS))
|
|
248
|
+
new_change: float = abs(self.distance - new_distance)
|
|
249
|
+
self.distance = new_distance
|
|
250
|
+
self.change = new_change
|
|
251
|
+
return self.distance, self.change
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def data(self) -> RadarData:
|
|
255
|
+
# noinspection PyTypeChecker
|
|
256
|
+
return RadarData(
|
|
257
|
+
name=self.name,
|
|
258
|
+
ship_distance=self.distance,
|
|
259
|
+
distance_change=self.change,
|
|
260
|
+
distance_status="ACTIVE" if self.active else "INACTIVE",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class BerthWorker:
|
|
265
|
+
"""a worker class for generating and managing changes in Berth data."""
|
|
266
|
+
|
|
267
|
+
def __init__(self, berth_code: str):
|
|
268
|
+
self.berth_code: str = berth_code
|
|
269
|
+
self.bent_count: int = random.randint(BENT_COUNT_MIN, BENT_COUNT_MAX)
|
|
270
|
+
self.hook_count: int = self.bent_count * HOOK_COUNT_MULTIPLIER
|
|
271
|
+
self.ship: ShipData = generate_ship()
|
|
272
|
+
self.radars: list[RadarWorker] = []
|
|
273
|
+
for radar_num in range(1, random.choice([5, 6, 6, 6]) + 1):
|
|
274
|
+
radar_name = f"B{berth_code}RD{radar_num}"
|
|
275
|
+
self.radars.append(RadarWorker(radar_name))
|
|
276
|
+
|
|
277
|
+
self.bents: list[BentWorker] = []
|
|
278
|
+
for bent_num in range(1, self.bent_count + 1):
|
|
279
|
+
self.bents.append(BentWorker(bent_num, self.bent_count))
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def name(self) -> str:
|
|
283
|
+
return f"Berth {self.berth_code}"
|
|
284
|
+
|
|
285
|
+
def update(self):
|
|
286
|
+
for radar in self.radars:
|
|
287
|
+
radar.update()
|
|
288
|
+
for bent in self.bents:
|
|
289
|
+
bent.update()
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def data(self) -> BerthData:
|
|
293
|
+
return BerthData(
|
|
294
|
+
name=self.name,
|
|
295
|
+
bent_count=self.bent_count,
|
|
296
|
+
hook_count=self.hook_count,
|
|
297
|
+
ship=self.ship,
|
|
298
|
+
radars=[radar.data for radar in self.radars],
|
|
299
|
+
bents=[bent.data for bent in self.bents],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class PortWorker:
|
|
304
|
+
"""a worker class for generating and managing change of ports"""
|
|
305
|
+
|
|
306
|
+
def __init__(self):
|
|
307
|
+
self.name: str = random_wa_port_name()
|
|
308
|
+
self.berth_count: int = random.randint(1, 8)
|
|
309
|
+
self.berths: list[BerthWorker] = []
|
|
310
|
+
for berth_num in range(1, self.berth_count + 1):
|
|
311
|
+
berth_code: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[berth_num]
|
|
312
|
+
self.berths.append(BerthWorker(berth_code))
|
|
313
|
+
|
|
314
|
+
def update(self):
|
|
315
|
+
for berth in self.berths:
|
|
316
|
+
berth.update()
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def data(self) -> PortData:
|
|
320
|
+
return PortData(
|
|
321
|
+
name=self.name,
|
|
322
|
+
berths=[berth.data for berth in self.berths],
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def build_random_port() -> PortWorker:
|
|
327
|
+
"""Construct a `PortData` instance with a random WA port name."""
|
|
328
|
+
return PortWorker()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def main() -> None:
|
|
332
|
+
"""Generate a single random WA port and print it as JSON."""
|
|
333
|
+
port = build_random_port()
|
|
334
|
+
# Use Pydantic's by_alias to apply PascalCase field names from BasePayloadModel
|
|
335
|
+
payload = port.data.model_dump(by_alias=True)
|
|
336
|
+
logger.info(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
if __name__ == "__main__":
|
|
340
|
+
main()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from . import file_worker, http_worker
|
|
6
|
+
from .openapi import generate_openapi_spec
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
parser = argparse.ArgumentParser(description="Mooring data generator")
|
|
11
|
+
parser.add_argument(
|
|
12
|
+
"url", nargs="?", help="HTTP endpoint URL (required if --file is not provided)"
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument("--file", type=str, help="Path to output JSON file (e.g., path/filename.json)")
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--openapi",
|
|
17
|
+
action="store_true",
|
|
18
|
+
help="Output OpenAPI specification for the data output instead of running the generator",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
"""Run the cli tooling for mooring data generator"""
|
|
24
|
+
args = parser.parse_args()
|
|
25
|
+
|
|
26
|
+
# Handle --openapi flag
|
|
27
|
+
if args.openapi:
|
|
28
|
+
spec = generate_openapi_spec()
|
|
29
|
+
print(spec)
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
# Validate that either url or --file is provided
|
|
33
|
+
if not args.url and not args.file:
|
|
34
|
+
parser.error("Either url or --file must be provided")
|
|
35
|
+
|
|
36
|
+
if args.url and args.file:
|
|
37
|
+
parser.error("Cannot use both url and --file at the same time")
|
|
38
|
+
|
|
39
|
+
if args.file:
|
|
40
|
+
# Use file_worker
|
|
41
|
+
file_path = Path(args.file)
|
|
42
|
+
logger.info(f"Starting mooring data generator and will save to files: {file_path}")
|
|
43
|
+
print(f"Starting mooring data generator and will save to files: {file_path}")
|
|
44
|
+
print("Press CTRL+C to stop mooring data generator.")
|
|
45
|
+
file_worker.run(file_path)
|
|
46
|
+
else:
|
|
47
|
+
# Use http_worker
|
|
48
|
+
url: str = args.url
|
|
49
|
+
logger.info(f"Starting mooring data generator and will HTTP POST to {url}")
|
|
50
|
+
print(f"Starting mooring data generator and will HTTP POST to {url}")
|
|
51
|
+
print("Press CTRL+C to stop mooring data generator.")
|
|
52
|
+
http_worker.run(url)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
main()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .builder import build_random_port
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(path: Path) -> None:
|
|
14
|
+
"""Continuously generate port data and save to JSON files every 2 seconds.
|
|
15
|
+
|
|
16
|
+
- Builds a random port via builder.build_random_port(), store in variable `port`.
|
|
17
|
+
- Forever loop until interrupted (Ctrl+C):
|
|
18
|
+
- Generate filename using pattern: path.parent / f"{path.stem}_{timestamp}{path.suffix}"
|
|
19
|
+
- Save JSON payload from `port.data.model_dump(by_alias=True)` to the file.
|
|
20
|
+
If the file write fails, print a message to stdout and continue.
|
|
21
|
+
- Call `port.update()` to mutate the generated data for next iteration.
|
|
22
|
+
- Sleep for 2 seconds.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
path: Pathlib Path object used to build the output filename pattern
|
|
26
|
+
"""
|
|
27
|
+
port = build_random_port()
|
|
28
|
+
loops = 0
|
|
29
|
+
payloads = 1
|
|
30
|
+
try:
|
|
31
|
+
while True:
|
|
32
|
+
loops += 1
|
|
33
|
+
try:
|
|
34
|
+
print(f" loop: {loops:<8} saving payload: {payloads}", end="\r")
|
|
35
|
+
payload = port.data.model_dump(by_alias=True)
|
|
36
|
+
|
|
37
|
+
# Build filename with timestamp
|
|
38
|
+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
39
|
+
output_file = path.parent / f"{path.stem}_{timestamp}{path.suffix}"
|
|
40
|
+
|
|
41
|
+
# Ensure parent directory exists
|
|
42
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
# Write JSON to file
|
|
45
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
46
|
+
json.dump(payload, f, indent=2)
|
|
47
|
+
|
|
48
|
+
payloads += 1
|
|
49
|
+
except (OSError, IOError) as e:
|
|
50
|
+
# Notify stdout on failure but continue processing
|
|
51
|
+
logger.error(f"File write failed: {e}")
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"Unknown Error: {e}")
|
|
55
|
+
logger.exception(e)
|
|
56
|
+
finally:
|
|
57
|
+
# Update model and wait regardless of write success
|
|
58
|
+
try:
|
|
59
|
+
port.update()
|
|
60
|
+
except Exception as e: # Defensive: updating should not kill the loop
|
|
61
|
+
logger.error(f"Port update failed: {e}")
|
|
62
|
+
logger.exception(e)
|
|
63
|
+
raise e
|
|
64
|
+
time.sleep(2)
|
|
65
|
+
except KeyboardInterrupt:
|
|
66
|
+
# Graceful shutdown on Ctrl+C
|
|
67
|
+
logger.info("Interrupted by user. Exiting.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
# Allow running this module directly: python -m mooring_data_generator.file_worker <PATH>
|
|
72
|
+
if len(sys.argv) != 2:
|
|
73
|
+
print("Usage: file_worker.py <PATH>")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
run(Path(sys.argv[1]))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
|
|
8
|
+
from .builder import build_random_port
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(url: str) -> None:
|
|
14
|
+
"""Continuously POST generated port data to the given URL every 2 seconds.
|
|
15
|
+
|
|
16
|
+
- Builds a random port via builder.build_random_port(), store in variable `port`.
|
|
17
|
+
- Forever loop until interrupted (Ctrl+C):
|
|
18
|
+
- Send JSON payload from `port.data.model_dump(by_alias=True)` to the URL.
|
|
19
|
+
If the HTTP request fails, print a message to stdout and continue.
|
|
20
|
+
- Call `port.update()` to mutate the generated data for next iteration.
|
|
21
|
+
- Sleep for 2 seconds.
|
|
22
|
+
"""
|
|
23
|
+
port = build_random_port()
|
|
24
|
+
loops = 0
|
|
25
|
+
payloads = 1
|
|
26
|
+
try:
|
|
27
|
+
while True:
|
|
28
|
+
loops += 1
|
|
29
|
+
try:
|
|
30
|
+
print(f" loop: {loops:<8} sending payload: {payloads}", end="\r")
|
|
31
|
+
payload = port.data.model_dump(by_alias=True)
|
|
32
|
+
data_bytes = json.dumps(payload).encode("utf-8")
|
|
33
|
+
req = urllib.request.Request(
|
|
34
|
+
url,
|
|
35
|
+
data=data_bytes,
|
|
36
|
+
headers={"Content-Type": "application/json"},
|
|
37
|
+
method="POST",
|
|
38
|
+
)
|
|
39
|
+
with urllib.request.urlopen(req, timeout=10) as resp: # noqa: S310 (stdlib only)
|
|
40
|
+
# We don't need to print on success; quietly proceed.
|
|
41
|
+
_ = resp.read()
|
|
42
|
+
payloads += 1
|
|
43
|
+
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError) as e:
|
|
44
|
+
# Notify stdout on failure but continue processing
|
|
45
|
+
logger.error(f"HTTP send failed: {e}")
|
|
46
|
+
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Unknown Error: {e}")
|
|
49
|
+
logger.exception(e)
|
|
50
|
+
finally:
|
|
51
|
+
# Update model and wait regardless of send success
|
|
52
|
+
try:
|
|
53
|
+
port.update()
|
|
54
|
+
except Exception as e: # Defensive: updating should not kill the loop
|
|
55
|
+
logger.error(f"Port update failed: {e}")
|
|
56
|
+
logger.exception(e)
|
|
57
|
+
raise e
|
|
58
|
+
time.sleep(2)
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
# Graceful shutdown on Ctrl+C
|
|
61
|
+
logger.info("Interrupted by user. Exiting.")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
# Allow running this module directly: python -m mooring_data_generator.http_worker <URL>
|
|
66
|
+
if len(sys.argv) != 2:
|
|
67
|
+
print("Usage: http_worker.py <URL>")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
run(sys.argv[1])
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import Annotated, Literal, TypedDict
|
|
2
|
+
|
|
3
|
+
from pydantic import AliasGenerator, BaseModel, ConfigDict, Field, alias_generators
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TensionLimits(TypedDict):
|
|
7
|
+
high_tension: int
|
|
8
|
+
medium_tension: int
|
|
9
|
+
low_tension: int
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
TENSION_LIMITS: dict[str, TensionLimits] = {
|
|
13
|
+
"default": TensionLimits(
|
|
14
|
+
high_tension=24,
|
|
15
|
+
medium_tension=14,
|
|
16
|
+
low_tension=4,
|
|
17
|
+
),
|
|
18
|
+
"Berth A": TensionLimits(
|
|
19
|
+
high_tension=25,
|
|
20
|
+
medium_tension=15,
|
|
21
|
+
low_tension=5,
|
|
22
|
+
),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BasePayloadModel(BaseModel):
|
|
27
|
+
model_config = ConfigDict(
|
|
28
|
+
alias_generator=AliasGenerator(
|
|
29
|
+
serialization_alias=alias_generators.to_pascal,
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ShipData(BasePayloadModel):
|
|
35
|
+
name: str
|
|
36
|
+
vessel_id: Annotated[str, Field(pattern=r"^[0-9]{4}$")]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RadarData(BasePayloadModel):
|
|
40
|
+
name: Annotated[str, Field(pattern=r"^B[A-Z]RD[0-9]$")]
|
|
41
|
+
# Radar Should share the name of the Berth, Berth A Radar 1 = BARD1
|
|
42
|
+
ship_distance: Annotated[float, Field(ge=0, lt=100)] | None
|
|
43
|
+
# min 2.8 - 6.7 | max 3.4 - 30.7
|
|
44
|
+
distance_change: Annotated[float, Field(gt=-100, lt=100)] | None
|
|
45
|
+
# min 0.0007 - 0.06 | max 0.029 - 5.76
|
|
46
|
+
distance_status: Literal["INACTIVE", "ACTIVE"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HookData(BasePayloadModel):
|
|
50
|
+
name: Annotated[str, Field(pattern=r"^Hook [1-9][0-9]?$")]
|
|
51
|
+
tension: Annotated[int, Field(ge=0, lt=99)] | None
|
|
52
|
+
faulted: bool
|
|
53
|
+
attached_line: Literal["BREAST", "HEAD", "SPRING", "STERN"] | None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BentData(BasePayloadModel):
|
|
57
|
+
name: Annotated[str, Field(pattern=r"^BNT[0-9]{3}$")]
|
|
58
|
+
# naming convention BNT + unique id
|
|
59
|
+
hooks: list[HookData]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BerthData(BasePayloadModel):
|
|
63
|
+
name: Annotated[str, Field(pattern=r"^Berth [A-Z]$")]
|
|
64
|
+
bent_count: Annotated[int, Field(gt=0, lt=40)] # the count of bents: min 9 | max 15
|
|
65
|
+
hook_count: Annotated[int, Field(gt=0, lt=60)] # the count of hooks: min 27 | max 48
|
|
66
|
+
ship: ShipData
|
|
67
|
+
radars: list[RadarData]
|
|
68
|
+
bents: list[BentData]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PortData(BasePayloadModel):
|
|
72
|
+
name: str # the name of the port
|
|
73
|
+
berths: list[BerthData]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from .models import PortData
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_openapi_spec() -> str:
|
|
7
|
+
"""Generate an OpenAPI 3.0 specification for the mooring data output.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
JSON string of the OpenAPI specification
|
|
11
|
+
"""
|
|
12
|
+
# Get the Pydantic model's JSON schema
|
|
13
|
+
port_schema = PortData.model_json_schema()
|
|
14
|
+
|
|
15
|
+
# Build OpenAPI 3.0 specification
|
|
16
|
+
openapi_spec = {
|
|
17
|
+
"openapi": "3.0.3",
|
|
18
|
+
"info": {
|
|
19
|
+
"title": "Mooring Data Generator API",
|
|
20
|
+
"description": "API specification for mooring data generated by mooring-data-generator",
|
|
21
|
+
"version": "1.0.0",
|
|
22
|
+
},
|
|
23
|
+
"paths": {
|
|
24
|
+
"/": {
|
|
25
|
+
"post": {
|
|
26
|
+
"summary": "Receive mooring data",
|
|
27
|
+
"description": "Endpoint that receives generated mooring data via HTTP POST",
|
|
28
|
+
"requestBody": {
|
|
29
|
+
"required": True,
|
|
30
|
+
"content": {
|
|
31
|
+
"application/json": {
|
|
32
|
+
"schema": {"$ref": "#/components/schemas/PortData"}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
"responses": {"200": {"description": "Successfully received mooring data"}},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"components": {"schemas": {}},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Extract all schemas from the Pydantic model
|
|
44
|
+
# The model_json_schema() includes all nested models in $defs
|
|
45
|
+
if "$defs" in port_schema:
|
|
46
|
+
openapi_spec["components"]["schemas"] = port_schema["$defs"]
|
|
47
|
+
|
|
48
|
+
# Add the main PortData schema
|
|
49
|
+
port_data_schema = {k: v for k, v in port_schema.items() if k != "$defs"}
|
|
50
|
+
openapi_spec["components"]["schemas"]["PortData"] = port_data_schema
|
|
51
|
+
|
|
52
|
+
return json.dumps(openapi_spec, indent=2)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
5
|
+
|
|
6
|
+
DEFAULT_PORT = 8000
|
|
7
|
+
MAX_SHOW = 1024 * 1
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PrintingRequestHandler(BaseHTTPRequestHandler):
|
|
11
|
+
"""A very simple request handler that prints the full request and responds 200.
|
|
12
|
+
|
|
13
|
+
- Prints: client address, method, path, HTTP version, headers, and body (if any)
|
|
14
|
+
- Responds: 200 OK with a small text/plain body
|
|
15
|
+
- Avoids using any external packages
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# Max bytes of body to show; set to None for full body
|
|
19
|
+
max_show: int = MAX_SHOW
|
|
20
|
+
|
|
21
|
+
# Format mode: None for default, "json" for JSON output
|
|
22
|
+
format_mode: bool = False
|
|
23
|
+
|
|
24
|
+
# Disable default logging to stderr; we print our own structured output
|
|
25
|
+
def log_message(self, format: str, *args) -> None: # noqa: A003 - match BaseHTTPRequestHandler
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
def _read_body(self) -> bytes:
|
|
29
|
+
length = int(self.headers.get("Content-Length", 0) or 0)
|
|
30
|
+
if length > 0:
|
|
31
|
+
return self.rfile.read(length)
|
|
32
|
+
return b""
|
|
33
|
+
|
|
34
|
+
def _print_body(self, body: bytes, content_type: str) -> None:
|
|
35
|
+
"""Safely limit body printout size (unless max_show is None"""
|
|
36
|
+
if self.format_mode:
|
|
37
|
+
if content_type == "application/json":
|
|
38
|
+
body = json.dumps(json.loads(body.decode("utf-8")), indent=2).encode("utf-8")
|
|
39
|
+
if self.max_show is None:
|
|
40
|
+
shown = body
|
|
41
|
+
else:
|
|
42
|
+
shown = body[: self.max_show]
|
|
43
|
+
try:
|
|
44
|
+
print(shown.decode("utf-8", errors="replace"))
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f"-- {e}")
|
|
47
|
+
print(repr(shown))
|
|
48
|
+
if self.max_show is not None and len(body) > self.max_show:
|
|
49
|
+
print(f"-- {len(body) - self.max_show} more bytes not shown --")
|
|
50
|
+
|
|
51
|
+
def _print_request(self, body: bytes) -> None:
|
|
52
|
+
"""print the request to stdoutmo"""
|
|
53
|
+
# First line
|
|
54
|
+
http_version = {
|
|
55
|
+
9: "HTTP/0.9",
|
|
56
|
+
10: "HTTP/1.0",
|
|
57
|
+
11: "HTTP/1.1",
|
|
58
|
+
}.get(self.protocol_version_number(), self.protocol_version)
|
|
59
|
+
|
|
60
|
+
client_host, client_port = self.client_address
|
|
61
|
+
print("=" * 80)
|
|
62
|
+
print(f"Client: {client_host}:{client_port}")
|
|
63
|
+
print(f"Request: {self.command} {self.path} {http_version}")
|
|
64
|
+
print("-- Headers --")
|
|
65
|
+
for k, v in self.headers.items():
|
|
66
|
+
print(f"{k}: {v}")
|
|
67
|
+
if body:
|
|
68
|
+
print("-- Body (bytes) --")
|
|
69
|
+
content_type = self.headers.get("Content-Type", "")
|
|
70
|
+
self._print_body(body, content_type)
|
|
71
|
+
else:
|
|
72
|
+
print("-- No Body --")
|
|
73
|
+
print("=" * 80)
|
|
74
|
+
sys.stdout.flush()
|
|
75
|
+
|
|
76
|
+
def _respond_ok(self) -> None:
|
|
77
|
+
message = b"OK\n"
|
|
78
|
+
self.send_response(200, "OK")
|
|
79
|
+
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
80
|
+
self.send_header("Content-Length", str(len(message)))
|
|
81
|
+
self.end_headers()
|
|
82
|
+
self.wfile.write(message)
|
|
83
|
+
|
|
84
|
+
# Map common methods to the same handler
|
|
85
|
+
def do_GET(self) -> None: # noqa: N802 - required by BaseHTTPRequestHandler
|
|
86
|
+
body = self._read_body()
|
|
87
|
+
self._print_request(body)
|
|
88
|
+
self._respond_ok()
|
|
89
|
+
|
|
90
|
+
def do_POST(self) -> None: # noqa: N802
|
|
91
|
+
body = self._read_body()
|
|
92
|
+
self._print_request(body)
|
|
93
|
+
self._respond_ok()
|
|
94
|
+
|
|
95
|
+
def do_PUT(self) -> None: # noqa: N802
|
|
96
|
+
body = self._read_body()
|
|
97
|
+
self._print_request(body)
|
|
98
|
+
self._respond_ok()
|
|
99
|
+
|
|
100
|
+
def do_PATCH(self) -> None: # noqa: N802
|
|
101
|
+
body = self._read_body()
|
|
102
|
+
self._print_request(body)
|
|
103
|
+
self._respond_ok()
|
|
104
|
+
|
|
105
|
+
def do_DELETE(self) -> None: # noqa: N802
|
|
106
|
+
body = self._read_body()
|
|
107
|
+
self._print_request(body)
|
|
108
|
+
self._respond_ok()
|
|
109
|
+
|
|
110
|
+
def do_OPTIONS(self) -> None: # noqa: N802
|
|
111
|
+
# Minimal CORS-friendly response
|
|
112
|
+
self.send_response(200, "OK")
|
|
113
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
114
|
+
self.send_header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
|
|
115
|
+
self.send_header("Access-Control-Allow-Headers", "*, Content-Type, Authorization")
|
|
116
|
+
self.end_headers()
|
|
117
|
+
|
|
118
|
+
# Helper to expose numeric protocol version (for display)
|
|
119
|
+
def protocol_version_number(self) -> int:
|
|
120
|
+
try:
|
|
121
|
+
return int(self.protocol_version.split("/")[-1].replace(".", ""))
|
|
122
|
+
except Exception:
|
|
123
|
+
return 11 # assume HTTP/1.1 if unknown
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def serve(port: int = DEFAULT_PORT, host: str = "0.0.0.0") -> tuple[str, int]:
|
|
127
|
+
"""Start the HTTP server and block forever.
|
|
128
|
+
|
|
129
|
+
Returns the actual bound address (host, port). Useful when passing port=0.
|
|
130
|
+
"""
|
|
131
|
+
server = ThreadingHTTPServer((host, port), PrintingRequestHandler)
|
|
132
|
+
bound_host, bound_port = server.server_address
|
|
133
|
+
print(f"Listening on http://{bound_host}:{bound_port} (press Ctrl+C to stop)")
|
|
134
|
+
try:
|
|
135
|
+
server.serve_forever()
|
|
136
|
+
except KeyboardInterrupt:
|
|
137
|
+
print("Shutting down...")
|
|
138
|
+
finally:
|
|
139
|
+
server.server_close()
|
|
140
|
+
return bound_host, bound_port
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cli(argv: list[str] | None = None) -> None:
|
|
144
|
+
parser = argparse.ArgumentParser(description="Simple HTTP request printer")
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--port",
|
|
147
|
+
"-p",
|
|
148
|
+
type=int,
|
|
149
|
+
default=DEFAULT_PORT,
|
|
150
|
+
help=f"Port to listen on (default: {DEFAULT_PORT})",
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--host",
|
|
154
|
+
"-H",
|
|
155
|
+
default="0.0.0.0",
|
|
156
|
+
help="Host/interface to bind (default: 0.0.0.0)",
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--full",
|
|
160
|
+
action="store_true",
|
|
161
|
+
help="Print full request bodies without truncation",
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--format",
|
|
165
|
+
action="store_true",
|
|
166
|
+
help="Output format for request content (json uses json.dumps, otherwise content as received)",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
args = parser.parse_args(argv)
|
|
170
|
+
|
|
171
|
+
# Configure handler truncation behavior
|
|
172
|
+
if args.full:
|
|
173
|
+
PrintingRequestHandler.max_show = None # show full body
|
|
174
|
+
else:
|
|
175
|
+
PrintingRequestHandler.max_show = MAX_SHOW
|
|
176
|
+
|
|
177
|
+
# Configure output format
|
|
178
|
+
if args.format:
|
|
179
|
+
PrintingRequestHandler.format_mode = args.format
|
|
180
|
+
|
|
181
|
+
serve(port=args.port, host=args.host)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
cli()
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mooring-data-generator
|
|
3
|
+
Version: 0.11.0
|
|
4
|
+
Summary: Random-data generator to simulate fictional port data flows for use at a Hackathon
|
|
5
|
+
Author: Ben Fitzhardinge
|
|
6
|
+
Author-email: Ben Fitzhardinge <ben@benjamin.dog>
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Topic :: Software Development :: Testing :: Traffic Generation
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: pydantic>=2.8
|
|
17
|
+
Maintainer: Ben Fitzhardinge
|
|
18
|
+
Maintainer-email: Ben Fitzhardinge <ben@benjamin.dog>
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Mooring Data Generator
|
|
23
|
+
|
|
24
|
+
A simple script to generate fake mooring data for use in a hackathon.
|
|
25
|
+
This script will send data payloads to and endpoint to simulate the data which might exist.
|
|
26
|
+
|
|
27
|
+
These will be http POST queries to the url provided as an argument at run time.
|
|
28
|
+
|
|
29
|
+
The script will run forever until the user sends a Ctrl+C command to end the script.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
### With UV (recommended)
|
|
34
|
+
|
|
35
|
+
If you don't have UV on your system, read [the install instructions for UV](https://docs.astral.sh/uv/getting-started/installation/)
|
|
36
|
+
|
|
37
|
+
```shell
|
|
38
|
+
uv tool install -U mooring-data-generator
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Vanilla python (If you don't want UV)
|
|
42
|
+
|
|
43
|
+
```shell
|
|
44
|
+
pip install -U mooring-data-generator
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### Running the package
|
|
50
|
+
|
|
51
|
+
#### Sending data via HTTP POST
|
|
52
|
+
|
|
53
|
+
```shell
|
|
54
|
+
mooring-data-generator http://127.0.0.1:8000/my/endpoint/
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> [!IMPORTANT]
|
|
58
|
+
> replace `http://127.0.0.1:8000/my/endpoint/` with the appropriate url for your system
|
|
59
|
+
|
|
60
|
+
#### Saving data to a file
|
|
61
|
+
|
|
62
|
+
You can also save the generated mooring data to a JSON file instead of sending it via HTTP:
|
|
63
|
+
|
|
64
|
+
```shell
|
|
65
|
+
mooring-data-generator --file output.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This will continuously generate mooring data and save it to the specified file.
|
|
69
|
+
|
|
70
|
+
> [!NOTE]
|
|
71
|
+
> You can only use either the URL (HTTP POST) or `--file` option, not both at the same time
|
|
72
|
+
|
|
73
|
+
#### Getting the OpenAPI specification
|
|
74
|
+
|
|
75
|
+
You can output an OpenAPI 3.0 specification for the mooring data format:
|
|
76
|
+
|
|
77
|
+
```shell
|
|
78
|
+
mooring-data-generator --openapi
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This will print a complete OpenAPI specification in JSON format
|
|
82
|
+
that describes the structure of the mooring data being generated.
|
|
83
|
+
This is useful for:
|
|
84
|
+
|
|
85
|
+
- Understanding the data format
|
|
86
|
+
- Generating client libraries
|
|
87
|
+
- API documentation
|
|
88
|
+
- Integration planning
|
|
89
|
+
|
|
90
|
+
The specification can be saved to a file for use with OpenAPI tools:
|
|
91
|
+
|
|
92
|
+
```shell
|
|
93
|
+
mooring-data-generator --openapi > openapi.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Testing data is being sent
|
|
97
|
+
|
|
98
|
+
There's a helper application included in this package
|
|
99
|
+
to allow you to check that the data is being sent.
|
|
100
|
+
|
|
101
|
+
`mooring-data-receiver` will display to the console all http traffic it receives.
|
|
102
|
+
|
|
103
|
+
```shell
|
|
104
|
+
mooring-data-receiver
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
By default it will run listening to any traffic `0.0.0.0` on port `8000`
|
|
108
|
+
|
|
109
|
+
You can adjust this if needed by using a commend like
|
|
110
|
+
|
|
111
|
+
```shell
|
|
112
|
+
mooring-data-receiver --host 127.0.0.1 --port 5000
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Formatting output
|
|
116
|
+
|
|
117
|
+
You can use the `--format` flag to control how the request body is displayed:
|
|
118
|
+
|
|
119
|
+
```shell
|
|
120
|
+
mooring-data-receiver --format
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
When `--format` is used, the request body content
|
|
124
|
+
will be formatted using `json.dumps(indent=2)` for better readability.
|
|
125
|
+
Without this flag, the content is displayed as received.
|
|
126
|
+
|
|
127
|
+
## Troubleshooting
|
|
128
|
+
|
|
129
|
+
### Command not found
|
|
130
|
+
|
|
131
|
+
If you are having trouble with the command not being found,
|
|
132
|
+
you can attempt to run it as a module calling python
|
|
133
|
+
|
|
134
|
+
```shell
|
|
135
|
+
python -m mooring-data-generator http://127.0.0.1:8000/my/endpoint/
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Pip not found
|
|
139
|
+
|
|
140
|
+
If `pip` can't be found on your system.
|
|
141
|
+
|
|
142
|
+
First, make sure you have Python installed.
|
|
143
|
+
|
|
144
|
+
```shell
|
|
145
|
+
python --version
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
you can call `pip` from python directly as a module.
|
|
149
|
+
|
|
150
|
+
```shell
|
|
151
|
+
python -m pip install -U mooring-data-generator
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Release a new version
|
|
155
|
+
|
|
156
|
+
### Be sure the tests pass
|
|
157
|
+
|
|
158
|
+
```shell
|
|
159
|
+
uv sync --all-groups
|
|
160
|
+
uv run ruff format
|
|
161
|
+
uv run ruff check
|
|
162
|
+
uv run tox
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### bump version and tag new release
|
|
166
|
+
|
|
167
|
+
```shell
|
|
168
|
+
uv version --bump minor
|
|
169
|
+
git commit -am "Release version v$(uv version --short)"
|
|
170
|
+
git tag -a "v$(uv version --short)" -m "v$(uv version --short)"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### push to github
|
|
174
|
+
|
|
175
|
+
```shell
|
|
176
|
+
git push
|
|
177
|
+
git push --tags
|
|
178
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
mooring_data_generator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mooring_data_generator/builder.py,sha256=CzNLpNvBaRDWfcbNFdiL-HFqLK5a4xoCn3dLzmNo9NQ,9096
|
|
3
|
+
mooring_data_generator/cli.py,sha256=H6jILYMEa6M-cwmHuD1UhGqSbiiH4FRQ8RxUtxxfa1Q,1821
|
|
4
|
+
mooring_data_generator/file_worker.py,sha256=PbXF7oWNaqsUCFzxRK5QETg6oDtHl7cUNYqWa8cyw5g,2721
|
|
5
|
+
mooring_data_generator/http_worker.py,sha256=4Aa9jkpNAOIXZFJtfUI8Orn2RmZE2UDmsiRV6-Pd56Y,2586
|
|
6
|
+
mooring_data_generator/models.py,sha256=TaJcnZ5SAzhgmaIdrZAMuzB_pUQqJtK04OcIgomb3aI,2087
|
|
7
|
+
mooring_data_generator/openapi.py,sha256=zLkBYRu5n0RuoTkoUwGx4zEGKSWktbFoQ86AXGoisvk,1799
|
|
8
|
+
mooring_data_generator/receiver.py,sha256=IevBKmdJket4qMb4zW41GDeBSjvyDriBJDjmyHIVFtg,6186
|
|
9
|
+
mooring_data_generator-0.11.0.dist-info/WHEEL,sha256=DpNsHFUm_gffZe1FgzmqwuqiuPC6Y-uBCzibcJcdupM,78
|
|
10
|
+
mooring_data_generator-0.11.0.dist-info/entry_points.txt,sha256=ZfpADZ8zW3FjJOgNafj14t_Va-Ga0ddlL9WEAYXHnjM,136
|
|
11
|
+
mooring_data_generator-0.11.0.dist-info/METADATA,sha256=ivbU4yLIxvbv_j914a7Spip9HzautsxYvBs4bC7h-pM,4369
|
|
12
|
+
mooring_data_generator-0.11.0.dist-info/RECORD,,
|