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.

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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.8
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ mooring-data-generator = mooring_data_generator.cli:main
3
+ mooring-data-receiver = mooring_data_receiver.http_server:cli
4
+