mooring-data-generator 0.1.0a5__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.
Potentially problematic release.
This version of mooring-data-generator might be problematic. Click here for more details.
- mooring_data_generator/__init__.py +0 -0
- mooring_data_generator/builder.py +340 -0
- mooring_data_generator/cli.py +25 -0
- mooring_data_generator/http_worker.py +69 -0
- mooring_data_generator/models.py +73 -0
- mooring_data_generator-0.1.0a5.dist-info/METADATA +94 -0
- mooring_data_generator-0.1.0a5.dist-info/RECORD +9 -0
- mooring_data_generator-0.1.0a5.dist-info/WHEEL +4 -0
- mooring_data_generator-0.1.0a5.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,25 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from .http_worker import run
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
parser = argparse.ArgumentParser(description="Mooring data generator")
|
|
9
|
+
parser.add_argument("url")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
"""Run the cli tooling for mooring data generator"""
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
url: str = args.url
|
|
16
|
+
|
|
17
|
+
# build a random structure for this port
|
|
18
|
+
logger.info(f"Starting mooring data generator and will HTTP POST to {url}")
|
|
19
|
+
print(f"Starting mooring data generator and will HTTP POST to {url}")
|
|
20
|
+
print("Press CTRL+C to stop mooring data generator.")
|
|
21
|
+
run(url)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
main()
|
|
@@ -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, indent=2).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,94 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mooring-data-generator
|
|
3
|
+
Version: 0.1.0a5
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Dist: pydantic>=2.8
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# Mooring Data Generator
|
|
10
|
+
|
|
11
|
+
A simple script to generate fake mooring data for use in a hackathon.
|
|
12
|
+
This script will send data payloads to and endpoint to simulate the data which might exist.
|
|
13
|
+
|
|
14
|
+
These will be http POST queries to the url provided as an argument at run time.
|
|
15
|
+
|
|
16
|
+
The script will run forever until the user sends a Ctrl+C command to end the script.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### With UV (recommended)
|
|
21
|
+
|
|
22
|
+
If you don't have UV on your system, read [the install instructions for UV](https://docs.astral.sh/uv/getting-started/installation/)
|
|
23
|
+
|
|
24
|
+
```shell
|
|
25
|
+
uvx mooring-data-generator http://127.0.0.1:8000/my/endpoint/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
[//]: # (TODO: this needs to be confirmed after we release the package to PyPI)
|
|
29
|
+
|
|
30
|
+
> [!IMPORTANT]
|
|
31
|
+
> replace `http://127.0.0.1:8000/my/endpoint/` with the appropriate url for your system
|
|
32
|
+
|
|
33
|
+
### Vanilla python (If you don't want UV)
|
|
34
|
+
|
|
35
|
+
#### Install the package
|
|
36
|
+
|
|
37
|
+
```shell
|
|
38
|
+
pip install -U mooring-data-generator
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Running the package
|
|
42
|
+
|
|
43
|
+
```shell
|
|
44
|
+
mooring-data-generator http://127.0.0.1:8000/my/endpoint/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> [!IMPORTANT]
|
|
48
|
+
> replace `http://127.0.0.1:8000/my/endpoint/` with the appropriate url for your system
|
|
49
|
+
|
|
50
|
+
## Testing data is being sent
|
|
51
|
+
|
|
52
|
+
There's a helper application included in this package
|
|
53
|
+
to allow you to check that the data is being sent.
|
|
54
|
+
|
|
55
|
+
`mooring-data-receiver` will display to the console all http traffic it receives.
|
|
56
|
+
|
|
57
|
+
```shell
|
|
58
|
+
mooring-data-receiver
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
By default it will run listening to any traffic `0.0.0.0` on port `8000`
|
|
62
|
+
|
|
63
|
+
You can adjust this if needed by using a commend like
|
|
64
|
+
|
|
65
|
+
```shell
|
|
66
|
+
mooring-data-receiver --host 127.0.0.1 --port 5000
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Troubleshooting
|
|
70
|
+
|
|
71
|
+
### Command not found
|
|
72
|
+
|
|
73
|
+
If you are having trouble with the command not being found,
|
|
74
|
+
you can attempt to run it as a module calling python
|
|
75
|
+
|
|
76
|
+
```shell
|
|
77
|
+
python -m mooring-data-generator http://127.0.0.1:8000/my/endpoint/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Pip not found
|
|
81
|
+
|
|
82
|
+
If `pip` can't be found on your system.
|
|
83
|
+
|
|
84
|
+
First, make sure you have Python installed.
|
|
85
|
+
|
|
86
|
+
```shell
|
|
87
|
+
python --version
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
you can call `pip` from python directly as a module.
|
|
91
|
+
|
|
92
|
+
```shell
|
|
93
|
+
python -m pip install -U mooring-data-generator
|
|
94
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
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=JvvIOSSFTpkSpWvx57r-rk2Mp5KAtDyZHflACgwbjHU,643
|
|
4
|
+
mooring_data_generator/http_worker.py,sha256=ZLXeiFA7CgZ-qYogyZKKScVNx5-xfUYIN39YHl-qtBY,2596
|
|
5
|
+
mooring_data_generator/models.py,sha256=TaJcnZ5SAzhgmaIdrZAMuzB_pUQqJtK04OcIgomb3aI,2087
|
|
6
|
+
mooring_data_generator-0.1.0a5.dist-info/WHEEL,sha256=DpNsHFUm_gffZe1FgzmqwuqiuPC6Y-uBCzibcJcdupM,78
|
|
7
|
+
mooring_data_generator-0.1.0a5.dist-info/entry_points.txt,sha256=xtj4Zzwe_n6MQyXQbWhDhjH97oY2SlIuwhfFgIXRT5I,138
|
|
8
|
+
mooring_data_generator-0.1.0a5.dist-info/METADATA,sha256=-s6Abnho7vA9pnttNOGUBRPW-W8BchlAW1Bw_8hq6eY,2247
|
|
9
|
+
mooring_data_generator-0.1.0a5.dist-info/RECORD,,
|