mooring-data-generator 0.9.0__tar.gz

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.

@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.3
2
+ Name: mooring-data-generator
3
+ Version: 0.9.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
+ ## Usage
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
+ uvx mooring-data-generator http://127.0.0.1:8000/my/endpoint/
39
+ ```
40
+
41
+ [//]: # (TODO: this needs to be confirmed after we release the package to PyPI)
42
+
43
+ > [!IMPORTANT]
44
+ > replace `http://127.0.0.1:8000/my/endpoint/` with the appropriate url for your system
45
+
46
+ ### Vanilla python (If you don't want UV)
47
+
48
+ #### Install the package
49
+
50
+ ```shell
51
+ pip install -U mooring-data-generator
52
+ ```
53
+
54
+ ### Running the package
55
+
56
+ ```shell
57
+ mooring-data-generator http://127.0.0.1:8000/my/endpoint/
58
+ ```
59
+
60
+ > [!IMPORTANT]
61
+ > replace `http://127.0.0.1:8000/my/endpoint/` with the appropriate url for your system
62
+
63
+ ## Testing data is being sent
64
+
65
+ There's a helper application included in this package
66
+ to allow you to check that the data is being sent.
67
+
68
+ `mooring-data-receiver` will display to the console all http traffic it receives.
69
+
70
+ ```shell
71
+ mooring-data-receiver
72
+ ```
73
+
74
+ By default it will run listening to any traffic `0.0.0.0` on port `8000`
75
+
76
+ You can adjust this if needed by using a commend like
77
+
78
+ ```shell
79
+ mooring-data-receiver --host 127.0.0.1 --port 5000
80
+ ```
81
+
82
+ ## Troubleshooting
83
+
84
+ ### Command not found
85
+
86
+ If you are having trouble with the command not being found,
87
+ you can attempt to run it as a module calling python
88
+
89
+ ```shell
90
+ python -m mooring-data-generator http://127.0.0.1:8000/my/endpoint/
91
+ ```
92
+
93
+ ### Pip not found
94
+
95
+ If `pip` can't be found on your system.
96
+
97
+ First, make sure you have Python installed.
98
+
99
+ ```shell
100
+ python --version
101
+ ```
102
+
103
+ you can call `pip` from python directly as a module.
104
+
105
+ ```shell
106
+ python -m pip install -U mooring-data-generator
107
+ ```
108
+
109
+ ## Release a new version
110
+
111
+ ### Be sure the tests pass
112
+
113
+ ```shell
114
+ uv run ruff format
115
+ uv run ruff check
116
+ uv run tox
117
+ ```
118
+
119
+ ### bump version and tag new release
120
+
121
+ ```shell
122
+ uv version --bump minor
123
+ git commit -am "Release version v$(uv version --short)"
124
+ git tag -a "v$(uv version --short)" -m "v$(uv version --short)"
125
+ ```
126
+
127
+ ### push to github
128
+
129
+ ```shell
130
+ git push
131
+ git push --tags
132
+ ```
@@ -0,0 +1,111 @@
1
+ # Mooring Data Generator
2
+
3
+ A simple script to generate fake mooring data for use in a hackathon.
4
+ This script will send data payloads to and endpoint to simulate the data which might exist.
5
+
6
+ These will be http POST queries to the url provided as an argument at run time.
7
+
8
+ The script will run forever until the user sends a Ctrl+C command to end the script.
9
+
10
+ ## Usage
11
+
12
+ ### With UV (recommended)
13
+
14
+ If you don't have UV on your system, read [the install instructions for UV](https://docs.astral.sh/uv/getting-started/installation/)
15
+
16
+ ```shell
17
+ uvx mooring-data-generator http://127.0.0.1:8000/my/endpoint/
18
+ ```
19
+
20
+ [//]: # (TODO: this needs to be confirmed after we release the package to PyPI)
21
+
22
+ > [!IMPORTANT]
23
+ > replace `http://127.0.0.1:8000/my/endpoint/` with the appropriate url for your system
24
+
25
+ ### Vanilla python (If you don't want UV)
26
+
27
+ #### Install the package
28
+
29
+ ```shell
30
+ pip install -U mooring-data-generator
31
+ ```
32
+
33
+ ### Running the package
34
+
35
+ ```shell
36
+ mooring-data-generator http://127.0.0.1:8000/my/endpoint/
37
+ ```
38
+
39
+ > [!IMPORTANT]
40
+ > replace `http://127.0.0.1:8000/my/endpoint/` with the appropriate url for your system
41
+
42
+ ## Testing data is being sent
43
+
44
+ There's a helper application included in this package
45
+ to allow you to check that the data is being sent.
46
+
47
+ `mooring-data-receiver` will display to the console all http traffic it receives.
48
+
49
+ ```shell
50
+ mooring-data-receiver
51
+ ```
52
+
53
+ By default it will run listening to any traffic `0.0.0.0` on port `8000`
54
+
55
+ You can adjust this if needed by using a commend like
56
+
57
+ ```shell
58
+ mooring-data-receiver --host 127.0.0.1 --port 5000
59
+ ```
60
+
61
+ ## Troubleshooting
62
+
63
+ ### Command not found
64
+
65
+ If you are having trouble with the command not being found,
66
+ you can attempt to run it as a module calling python
67
+
68
+ ```shell
69
+ python -m mooring-data-generator http://127.0.0.1:8000/my/endpoint/
70
+ ```
71
+
72
+ ### Pip not found
73
+
74
+ If `pip` can't be found on your system.
75
+
76
+ First, make sure you have Python installed.
77
+
78
+ ```shell
79
+ python --version
80
+ ```
81
+
82
+ you can call `pip` from python directly as a module.
83
+
84
+ ```shell
85
+ python -m pip install -U mooring-data-generator
86
+ ```
87
+
88
+ ## Release a new version
89
+
90
+ ### Be sure the tests pass
91
+
92
+ ```shell
93
+ uv run ruff format
94
+ uv run ruff check
95
+ uv run tox
96
+ ```
97
+
98
+ ### bump version and tag new release
99
+
100
+ ```shell
101
+ uv version --bump minor
102
+ git commit -am "Release version v$(uv version --short)"
103
+ git tag -a "v$(uv version --short)" -m "v$(uv version --short)"
104
+ ```
105
+
106
+ ### push to github
107
+
108
+ ```shell
109
+ git push
110
+ git push --tags
111
+ ```
@@ -0,0 +1,79 @@
1
+ [project]
2
+ name = "mooring-data-generator"
3
+ version = "0.9.0"
4
+ description = "Random-data generator to simulate fictional port data flows for use at a Hackathon"
5
+ authors = [
6
+ {name = "Ben Fitzhardinge", email = "ben@benjamin.dog"},
7
+ ]
8
+ maintainers = [
9
+ {name = "Ben Fitzhardinge", email = "ben@benjamin.dog"}
10
+ ]
11
+ classifiers = [
12
+ # How mature is this project? Common values are
13
+ # 3 - Alpha
14
+ # 4 - Beta
15
+ # 5 - Production/Stable
16
+ "Development Status :: 4 - Beta",
17
+ # Indicate who your project is intended for
18
+ "Intended Audience :: Developers",
19
+ "Topic :: Software Development :: Testing :: Traffic Generation",
20
+ # Specify the Python versions you support here.
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ ]
28
+
29
+ readme = "README.md"
30
+ requires-python = ">=3.10"
31
+ dependencies = [
32
+ "pydantic>=2.8",
33
+ ]
34
+
35
+ [project.scripts]
36
+ mooring-data-generator = "mooring_data_generator.cli:main"
37
+ mooring-data-receiver = "mooring_data_receiver.http_server:cli"
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "pytest~=8.4",
42
+ ]
43
+ test = [
44
+ "ruff>=0.14.4",
45
+ "tox~=4.32",
46
+ "tox-uv>=1.20.0",
47
+ ]
48
+ build = [
49
+ "uv-build~=0.9",
50
+ ]
51
+
52
+ # UV Build tooling
53
+ [build-system]
54
+ requires = ["uv_build>=0.9"]
55
+ build-backend = "uv_build"
56
+
57
+ # RUFF settings
58
+ [tool.ruff]
59
+ target-version = "py313"
60
+ line-length = 99
61
+
62
+ [tool.ruff.lint]
63
+ select = [
64
+ "C", # mccabe rules
65
+ "F", # pyflakes rules
66
+ "E", # pycodestyle error rules
67
+ "W", # pycodestyle warning rules
68
+ "B", # flake8-bugbear rules
69
+ "I", # isort rules
70
+ ]
71
+ ignore = [
72
+ # "C901", # max-complexity-10
73
+ "E501", # line-too-long
74
+ ]
75
+
76
+ # Rumdl settings
77
+ # This is a tool for formatting markdown files, it can be used to fix issues automatically.
78
+ [tool.rumdl]
79
+ line-length = 100
@@ -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]