goosebit 0.2.4__py3-none-any.whl → 0.2.5__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.
Files changed (34) hide show
  1. goosebit/__init__.py +16 -0
  2. goosebit/api/v1/devices/device/routes.py +2 -2
  3. goosebit/api/v1/devices/routes.py +19 -4
  4. goosebit/auth/__init__.py +5 -1
  5. goosebit/db/migrations/models/1_20241109151811_update.py +11 -0
  6. goosebit/db/models.py +6 -2
  7. goosebit/realtime/logs.py +1 -1
  8. goosebit/schema/devices.py +1 -1
  9. goosebit/settings/schema.py +2 -0
  10. goosebit/ui/bff/common/requests.py +3 -15
  11. goosebit/ui/bff/common/responses.py +16 -0
  12. goosebit/ui/bff/devices/responses.py +6 -2
  13. goosebit/ui/bff/devices/routes.py +53 -2
  14. goosebit/ui/bff/rollouts/responses.py +6 -2
  15. goosebit/ui/bff/routes.py +4 -2
  16. goosebit/ui/bff/software/responses.py +19 -9
  17. goosebit/ui/routes.py +7 -16
  18. goosebit/ui/static/js/devices.js +53 -69
  19. goosebit/ui/static/js/rollouts.js +16 -13
  20. goosebit/ui/static/js/software.js +5 -11
  21. goosebit/ui/static/js/util.js +21 -1
  22. goosebit/ui/templates/devices.html.jinja +0 -20
  23. goosebit/ui/templates/nav.html.jinja +13 -2
  24. goosebit/updater/controller/v1/routes.py +26 -20
  25. goosebit/updater/manager.py +20 -52
  26. goosebit/updater/routes.py +6 -2
  27. goosebit/updates/swdesc.py +1 -1
  28. {goosebit-0.2.4.dist-info → goosebit-0.2.5.dist-info}/METADATA +14 -6
  29. {goosebit-0.2.4.dist-info → goosebit-0.2.5.dist-info}/RECORD +32 -31
  30. {goosebit-0.2.4.dist-info → goosebit-0.2.5.dist-info}/WHEEL +1 -1
  31. goosebit-0.2.5.dist-info/entry_points.txt +3 -0
  32. goosebit/ui/static/js/index.js +0 -155
  33. goosebit/ui/templates/index.html.jinja +0 -25
  34. {goosebit-0.2.4.dist-info → goosebit-0.2.5.dist-info}/LICENSE +0 -0
@@ -6,24 +6,22 @@ document.addEventListener("DOMContentLoaded", async () => {
6
6
  paging: true,
7
7
  processing: true,
8
8
  serverSide: true,
9
- order: [1, "desc"],
9
+ order: {
10
+ name: "created_at",
11
+ dir: "desc",
12
+ },
10
13
  scrollCollapse: true,
11
14
  scroller: true,
12
15
  scrollY: "65vh",
13
16
  stateSave: true,
14
- stateLoadParams: (settings, data) => {
15
- // if save state is older than last breaking code change...
16
- if (data.time <= 1722413708000) {
17
- // ... delete it
18
- for (const key of Object.keys(data)) {
19
- delete data[key];
20
- }
21
- }
22
- },
23
17
  select: true,
24
18
  rowId: "id",
25
19
  ajax: {
26
20
  url: "/ui/bff/rollouts",
21
+ data: (data) => {
22
+ // biome-ignore lint/performance/noDelete: really has to be deleted
23
+ delete data.columns;
24
+ },
27
25
  contentType: "application/json",
28
26
  },
29
27
  initComplete: () => {
@@ -38,9 +36,14 @@ document.addEventListener("DOMContentLoaded", async () => {
38
36
  ],
39
37
  columns: [
40
38
  { data: "id", visible: false },
41
- { data: "created_at", orderable: true, render: (data) => new Date(data).toLocaleString() },
42
- { data: "name", searchable: true, orderable: true },
43
- { data: "feed", searchable: true, orderable: true },
39
+ {
40
+ data: "created_at",
41
+ name: "created_at",
42
+ orderable: true,
43
+ render: (data) => new Date(data).toLocaleString(),
44
+ },
45
+ { data: "name", name: "name", searchable: true, orderable: true },
46
+ { data: "feed", name: "feed", searchable: true, orderable: true },
44
47
  { data: "sw_file" },
45
48
  { data: "sw_version" },
46
49
  {
@@ -172,22 +172,16 @@ document.addEventListener("DOMContentLoaded", () => {
172
172
  paging: true,
173
173
  processing: false,
174
174
  serverSide: true,
175
- order: [2, "desc"],
176
175
  scrollCollapse: true,
177
176
  scroller: true,
178
177
  scrollY: "60vh",
179
178
  stateSave: true,
180
- stateLoadParams: (settings, data) => {
181
- // if save state is older than last breaking code change...
182
- if (data.time <= 1722415428000) {
183
- // ... delete it
184
- for (const key of Object.keys(data)) {
185
- delete data[key];
186
- }
187
- }
188
- },
189
179
  ajax: {
190
180
  url: "/ui/bff/software",
181
+ data: (data) => {
182
+ // biome-ignore lint/performance/noDelete: really has to be deleted
183
+ delete data.columns;
184
+ },
191
185
  contentType: "application/json",
192
186
  },
193
187
  initComplete: () => {
@@ -204,7 +198,7 @@ document.addEventListener("DOMContentLoaded", () => {
204
198
  columns: [
205
199
  { data: "id", visible: false },
206
200
  { data: "name" },
207
- { data: "version", searchable: true, orderable: true },
201
+ { data: "version", name: "version", searchable: true, orderable: true },
208
202
  {
209
203
  data: "compatibility",
210
204
  render: (data) => {
@@ -22,7 +22,7 @@ function secondsToRecentDate(t) {
22
22
 
23
23
  async function updateSoftwareSelection(devices = null) {
24
24
  try {
25
- const url = new URL("/ui/bff/software", window.location.origin);
25
+ const url = new URL("/ui/bff/software?order[0][dir]=desc&order[0][name]=version", window.location.origin);
26
26
  if (devices != null) {
27
27
  for (const device of devices) {
28
28
  url.searchParams.append("uuids", device.uuid);
@@ -64,6 +64,26 @@ async function updateSoftwareSelection(devices = null) {
64
64
  }
65
65
  }
66
66
 
67
+ async function get_request(url) {
68
+ const response = await fetch(url, {
69
+ method: "GET",
70
+ });
71
+
72
+ const result = await response.json();
73
+ if (!response.ok) {
74
+ if (result.detail) {
75
+ Swal.fire({
76
+ title: "Warning",
77
+ text: result.detail,
78
+ icon: "warning",
79
+ confirmButtonText: "Understood",
80
+ });
81
+ }
82
+
83
+ throw new Error(`POST ${url} failed for ${JSON.stringify(object)}`);
84
+ }
85
+ return result;
86
+ }
67
87
  async function post_request(url, object) {
68
88
  const response = await fetch(url, {
69
89
  method: "POST",
@@ -4,26 +4,6 @@
4
4
  <div class="row p-2 d-flex justify-content-center">
5
5
  <div class="col">
6
6
  <table id="device-table" class="table table-hover">
7
- <thead>
8
- <tr>
9
- <th>Up</th>
10
- <th>UUID</th>
11
- <th>Name</th>
12
- <th>Model</th>
13
- <th>Revision</th>
14
- <th>Feed</th>
15
- <th>Installed Software</th>
16
- <th>Target Software</th>
17
- <th>Update Mode</th>
18
- <th>State</th>
19
- <th>Force Update</th>
20
- <th>Progress</th>
21
- <th>Last IP</th>
22
- <th>Last Seen</th>
23
- </tr>
24
- </thead>
25
- <tbody id="devices-list">
26
- </tbody>
27
7
  </table>
28
8
  </div>
29
9
  </div>
@@ -60,13 +60,24 @@
60
60
  box-shadow: none!important;
61
61
  }
62
62
  </style>
63
- <script>const TABLE_UPDATE_TIME = 3000;</script>
63
+ <script>
64
+ const TABLE_UPDATE_TIME = 3000;
65
+ DataTable.ext.errMode = function ( e, settings, helpPage, message ) {
66
+ if (e.jqXHR.status == 401) {
67
+ window.location.reload();
68
+ } else if (e.jqXHR.status >= 200 && e.jqXHR.status < 300) {
69
+ console.error("AJAX query error when reloading datatable");
70
+ }
71
+ console.error("Unknown error when reloading datatable");
72
+ };
73
+
74
+ </script>
64
75
  <script src="{{ url_for('static', path='js/util.js') }}"></script>
65
76
  </head>
66
77
  <body data-bs-theme="dark">
67
78
  <nav class="navbar navbar-expand-lg bg-body-tertiary">
68
79
  <div class="container-fluid">
69
- <a class="navbar-brand" href="{{ request.url_for('home_ui') }}">
80
+ <a class="navbar-brand" href="{{ request.url_for('devices_ui') }}">
70
81
  <img src="{{ request.url_for('static', path='svg/goosebit-logo.svg') }}"
71
82
  class="me-2"
72
83
  height="30px"
@@ -2,7 +2,7 @@ import logging
2
2
 
3
3
  from fastapi import APIRouter, Depends, HTTPException
4
4
  from fastapi.requests import Request
5
- from fastapi.responses import FileResponse, RedirectResponse, Response
5
+ from fastapi.responses import FileResponse, Response
6
6
 
7
7
  from goosebit.db.models import Software, UpdateStateEnum
8
8
  from goosebit.settings import config
@@ -25,7 +25,6 @@ router = APIRouter(prefix="/v1")
25
25
  async def polling(request: Request, dev_id: str, updater: UpdateManager = Depends(get_update_manager)):
26
26
  links: dict[str, dict[str, str]] = {}
27
27
 
28
- sleep = updater.poll_time
29
28
  device = await updater.get_device()
30
29
 
31
30
  if device is None:
@@ -45,14 +44,15 @@ async def polling(request: Request, dev_id: str, updater: UpdateManager = Depend
45
44
  logger.info(f"Skip: registration required, device={updater.dev_id}")
46
45
 
47
46
  elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
48
- logger.warning(f"Skip: device in error state, device={updater.dev_id}")
49
- pass
47
+ sleep = config.poll_time_default
48
+ logger.info(f"Skip: device in error state, device={updater.dev_id}")
50
49
 
51
50
  else:
52
51
  # provide update if available. Note: this is also required while in state "running", otherwise swupdate
53
52
  # won't confirm a successful testing (might be a bug/problem in swupdate)
54
53
  handling_type, software = await updater.get_update()
55
54
  if handling_type != HandlingType.SKIP and software is not None:
55
+ sleep = config.poll_time_updating
56
56
  links["deploymentBase"] = {
57
57
  "href": str(
58
58
  request.url_for(
@@ -63,6 +63,11 @@ async def polling(request: Request, dev_id: str, updater: UpdateManager = Depend
63
63
  )
64
64
  }
65
65
  logger.info(f"Forced: update available, device={updater.dev_id}")
66
+ else:
67
+ sleep = config.poll_time_default
68
+
69
+ # update poll time on manager so that UI can properly display if device is overdue
70
+ updater.poll_time = sleep
66
71
 
67
72
  return {
68
73
  "config": {"polling": {"sleep": sleep}},
@@ -102,54 +107,55 @@ async def deployment_feedback(
102
107
  _: Request, data: FeedbackSchema, action_id: int, updater: UpdateManager = Depends(get_update_manager)
103
108
  ):
104
109
  if data.status.execution == FeedbackStatusExecutionState.PROCEEDING:
105
- await updater.update_device_state(UpdateStateEnum.RUNNING)
110
+ device = await updater.get_device()
111
+ if device and device.last_state != UpdateStateEnum.RUNNING:
112
+ await updater.clear_log()
113
+ await updater.update_device_state(UpdateStateEnum.RUNNING)
114
+
106
115
  logger.debug(f"Installation in progress, device={updater.dev_id}")
107
116
 
108
117
  elif data.status.execution == FeedbackStatusExecutionState.CLOSED:
109
118
  await updater.update_force_update(False)
110
- await updater.update_log_complete(True)
111
119
 
112
120
  reported_software = await Software.get_or_none(id=action_id)
113
121
 
114
122
  # From hawkBit docu: DDI defines also a status NONE which will not be interpreted by the update server
115
123
  # and handled like SUCCESS.
116
124
  if data.status.result.finished in [FeedbackStatusResultFinished.SUCCESS, FeedbackStatusResultFinished.NONE]:
125
+ await updater.deployment_action_success()
117
126
  await updater.update_device_state(UpdateStateEnum.FINISHED)
118
127
 
119
- # not guaranteed to be the correct rollout - see next comment.
120
128
  rollout = await updater.get_rollout()
121
129
  if rollout:
122
130
  if rollout.software == reported_software:
123
131
  rollout.success_count += 1
124
132
  await rollout.save()
125
133
  else:
134
+ # edge case where device update mode got changed while update was running
126
135
  logging.warning(
127
136
  f"Updating rollout success stats failed, software={reported_software.id}, device={updater.dev_id}" # noqa: E501
128
137
  )
129
138
 
130
- # setting the currently installed version based on the current assigned software / existing rollouts
131
- # is problematic. Better to assign custom action_id for each update (rollout id? software id? new id?).
132
- # Alternatively - but requires customization on the gateway side - use version reported by the gateway.
133
139
  await updater.update_sw_version(reported_software.version)
134
140
  logger.debug(f"Installation successful, software={reported_software.version}, device={updater.dev_id}")
135
141
 
136
142
  elif data.status.result.finished == FeedbackStatusResultFinished.FAILURE:
137
143
  await updater.update_device_state(UpdateStateEnum.ERROR)
138
144
 
139
- # not guaranteed to be the correct rollout - see comment above.
140
145
  rollout = await updater.get_rollout()
141
146
  if rollout:
142
147
  if rollout.software == reported_software:
143
148
  rollout.failure_count += 1
144
149
  await rollout.save()
145
150
  else:
151
+ # edge case where device update mode got changed while update was running
146
152
  logging.warning(
147
153
  f"Updating rollout failure stats failed, software={reported_software.id}, device={updater.dev_id}" # noqa: E501
148
154
  )
149
155
 
150
156
  logger.debug(f"Installation failed, software={reported_software.version}, device={updater.dev_id}")
151
157
  else:
152
- logging.warning(
158
+ logging.error(
153
159
  f"Device reported unhandled execution state, state={data.status.execution}, device={updater.dev_id}"
154
160
  )
155
161
 
@@ -179,11 +185,11 @@ async def download_artifact(_: Request, updater: UpdateManager = Depends(get_upd
179
185
  _, software = await updater.get_update()
180
186
  if software is None:
181
187
  raise HTTPException(404)
182
- if software.local:
183
- return FileResponse(
184
- software.path,
185
- media_type="application/octet-stream",
186
- filename=software.path.name,
187
- )
188
- else:
189
- return RedirectResponse(url=software.uri)
188
+
189
+ assert software.local, "device requests local software to download"
190
+
191
+ return FileResponse(
192
+ software.path,
193
+ media_type="application/octet-stream",
194
+ filename=software.path.name,
195
+ )
@@ -59,7 +59,7 @@ class UpdateManager(ABC):
59
59
  async def update_device_state(self, state: UpdateStateEnum) -> None:
60
60
  return
61
61
 
62
- async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
62
+ async def update_last_connection(self, last_seen: int, last_ip: str | None = None) -> None:
63
63
  return
64
64
 
65
65
  async def update_update(self, update_mode: UpdateModeEnum, software: Software | None):
@@ -74,7 +74,10 @@ class UpdateManager(ABC):
74
74
  async def update_config_data(self, **kwargs):
75
75
  return
76
76
 
77
- async def update_log_complete(self, log_complete: bool):
77
+ async def deployment_action_success(self):
78
+ return
79
+
80
+ async def clear_log(self) -> None:
78
81
  return
79
82
 
80
83
  async def get_rollout(self) -> Rollout | None:
@@ -83,9 +86,11 @@ class UpdateManager(ABC):
83
86
  @asynccontextmanager
84
87
  async def subscribe_log(self, callback: Callable):
85
88
  device = await self.get_device()
89
+ # do not modify, breaks when combined
86
90
  subscribers = self.log_subscribers
87
91
  subscribers.append(callback)
88
92
  self.log_subscribers = subscribers
93
+
89
94
  if device is not None:
90
95
  await callback(device.last_log)
91
96
  try:
@@ -93,6 +98,7 @@ class UpdateManager(ABC):
93
98
  except asyncio.CancelledError:
94
99
  pass
95
100
  finally:
101
+ # do not modify, breaks when combined
96
102
  subscribers = self.log_subscribers
97
103
  subscribers.remove(callback)
98
104
  self.log_subscribers = subscribers
@@ -133,27 +139,6 @@ class UpdateManager(ABC):
133
139
  async def update_log(self, log_data: str) -> None: ...
134
140
 
135
141
 
136
- class UnknownUpdateManager(UpdateManager):
137
- def __init__(self, dev_id: str):
138
- super().__init__(dev_id)
139
- self.poll_time = config.poll_time_updating
140
-
141
- async def _get_software(self) -> Software | None:
142
- device = await self.get_device()
143
- if device is None:
144
- return None
145
- return await Software.latest(device)
146
-
147
- async def get_update(self) -> tuple[HandlingType, Software | None]:
148
- software = await self._get_software()
149
- if software is None:
150
- return HandlingType.SKIP, None
151
- return HandlingType.FORCED, software
152
-
153
- async def update_log(self, log_data: str) -> None:
154
- return
155
-
156
-
157
142
  class DeviceUpdateManager(UpdateManager):
158
143
  hardware_default = None
159
144
 
@@ -193,10 +178,12 @@ class DeviceUpdateManager(UpdateManager):
193
178
  device.last_state = state
194
179
  await self.save_device(device, update_fields=["last_state"])
195
180
 
196
- async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
181
+ async def update_last_connection(self, last_seen: int, last_ip: str | None = None) -> None:
197
182
  device = await self.get_device()
198
183
  device.last_seen = last_seen
199
- if ":" in last_ip:
184
+ if last_ip is None:
185
+ await self.save_device(device, update_fields=["last_seen"])
186
+ elif ":" in last_ip:
200
187
  device.last_ipv6 = last_ip
201
188
  await self.save_device(device, update_fields=["last_seen", "last_ipv6"])
202
189
  else:
@@ -243,10 +230,10 @@ class DeviceUpdateManager(UpdateManager):
243
230
  if modified:
244
231
  await self.save_device(device, update_fields=["hardware_id", "last_state", "sw_version"])
245
232
 
246
- async def update_log_complete(self, log_complete: bool):
233
+ async def deployment_action_success(self):
247
234
  device = await self.get_device()
248
- device.log_complete = log_complete
249
- await self.save_device(device, update_fields=["log_complete"])
235
+ device.progress = 100
236
+ await self.save_device(device, update_fields=["progress"])
250
237
 
251
238
  async def get_rollout(self) -> Rollout | None:
252
239
  device = await self.get_device()
@@ -290,23 +277,15 @@ class DeviceUpdateManager(UpdateManager):
290
277
 
291
278
  if software is None:
292
279
  handling_type = HandlingType.SKIP
293
- self.poll_time = config.poll_time_default
294
280
 
295
281
  elif software.version == device.sw_version and not device.force_update:
296
282
  handling_type = HandlingType.SKIP
297
- self.poll_time = config.poll_time_default
298
283
 
299
284
  elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
300
285
  handling_type = HandlingType.SKIP
301
- self.poll_time = config.poll_time_default
302
286
 
303
287
  else:
304
288
  handling_type = HandlingType.FORCED
305
- self.poll_time = config.poll_time_updating
306
-
307
- if device.log_complete:
308
- await self.update_log_complete(False)
309
- await self.clear_log()
310
289
 
311
290
  return handling_type, software
312
291
 
@@ -318,23 +297,15 @@ class DeviceUpdateManager(UpdateManager):
318
297
  if device.last_log is None:
319
298
  device.last_log = ""
320
299
 
300
+ # SWUpdate-specific log parsing to report progress
321
301
  matches = re.findall(r"Downloaded (\d+)%", log_data)
322
302
  if matches:
323
303
  device.progress = matches[-1]
324
304
 
325
- if log_data.startswith("Installing Update Chunk Artifacts."):
326
- # clear log
327
- device.last_log = ""
328
- await self.publish_log(None)
329
-
330
- if not log_data == "Skipped Update.":
331
- device.last_log += f"{log_data}\n"
332
- await self.publish_log(f"{log_data}\n")
305
+ device.last_log += f"{log_data}\n"
306
+ await self.publish_log(f"{log_data}\n")
333
307
 
334
- await self.save_device(
335
- device,
336
- update_fields=["progress", "last_log"],
337
- )
308
+ await self.save_device(device, update_fields=["progress", "last_log"])
338
309
 
339
310
  async def clear_log(self) -> None:
340
311
  device = await self.get_device()
@@ -344,10 +315,7 @@ class DeviceUpdateManager(UpdateManager):
344
315
 
345
316
 
346
317
  async def get_update_manager(dev_id: str) -> UpdateManager:
347
- if dev_id == "unknown":
348
- return UnknownUpdateManager("unknown")
349
- else:
350
- return DeviceUpdateManager(dev_id)
318
+ return DeviceUpdateManager(dev_id)
351
319
 
352
320
 
353
321
  async def delete_devices(ids: list[str]):
@@ -3,14 +3,18 @@ import time
3
3
  from fastapi import APIRouter, Depends
4
4
  from fastapi.requests import Request
5
5
 
6
+ from goosebit.settings import config
7
+
6
8
  from . import controller
7
9
  from .manager import get_update_manager
8
10
 
9
11
 
10
12
  async def log_last_connection(request: Request, dev_id: str):
11
- host = request.client.host
12
13
  updater = await get_update_manager(dev_id)
13
- await updater.update_last_connection(round(time.time()), host)
14
+ if config.track_device_ip:
15
+ await updater.update_last_connection(round(time.time()), request.client.host)
16
+ else:
17
+ await updater.update_last_connection(round(time.time()))
14
18
 
15
19
 
16
20
  router = APIRouter(
@@ -23,7 +23,7 @@ def _append_compatibility(boardname, value, compatibility):
23
23
  def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
24
24
  swdesc_attrs = {}
25
25
  try:
26
- swdesc_attrs["version"] = semver.Version.parse(swdesc["software"]["version"])
26
+ swdesc_attrs["version"] = semver.Version.parse(swdesc["software"]["version"], optional_minor_and_patch=True)
27
27
  compatibility: list[dict[str, str]] = []
28
28
  _append_compatibility("default", swdesc["software"], compatibility)
29
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goosebit
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary:
5
5
  Author: Upstream Data
6
6
  Author-email: brett@upstreamdata.ca
@@ -8,6 +8,7 @@ Requires-Python: >=3.11,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
11
12
  Provides-Extra: postgresql
12
13
  Requires-Dist: aerich (>=0.7.2,<0.8.0)
13
14
  Requires-Dist: aiocache (>=0.12.2,<0.13.0)
@@ -19,9 +20,9 @@ Requires-Dist: itsdangerous (>=2.2.0,<3.0.0)
19
20
  Requires-Dist: jinja2 (>=3.1.4,<4.0.0)
20
21
  Requires-Dist: joserfc (>=1.0.0,<2.0.0)
21
22
  Requires-Dist: libconf (>=2.0.1,<3.0.0)
22
- Requires-Dist: opentelemetry-distro (>=0.47b0,<0.48)
23
- Requires-Dist: opentelemetry-exporter-prometheus (>=0.47b0,<0.48)
24
- Requires-Dist: opentelemetry-instrumentation-fastapi (>=0.47b0,<0.48)
23
+ Requires-Dist: opentelemetry-distro (>=0.49b1,<0.50)
24
+ Requires-Dist: opentelemetry-exporter-prometheus (>=0.49b1,<0.50)
25
+ Requires-Dist: opentelemetry-instrumentation-fastapi (>=0.49b1,<0.50)
25
26
  Requires-Dist: pydantic-settings (>=2.4.0,<3.0.0)
26
27
  Requires-Dist: python-multipart (>=0.0.9,<0.0.10)
27
28
  Requires-Dist: semver (>=3.0.2,<4.0.0)
@@ -33,6 +34,8 @@ Description-Content-Type: text/markdown
33
34
 
34
35
  <img src="docs/img/goosebit-logo.png" style="width: 100px; height: 100px; display: block;">
35
36
 
37
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/UpstreamDataInc/goosebit/badge)](https://scorecard.dev/viewer/?uri=github.com/UpstreamDataInc/goosebit)
38
+
36
39
  ---
37
40
 
38
41
  A simplistic, opinionated remote update server implementing hawkBit™'s [DDI API](https://eclipse.dev/hawkbit/apis/ddi_api/).
@@ -50,7 +53,6 @@ A simplistic, opinionated remote update server implementing hawkBit™'s [DDI AP
50
53
  2. Create the database:
51
54
 
52
55
  ```bash
53
- poetry run aerich init -t goosebit.db.config
54
56
  poetry run aerich upgrade
55
57
  ```
56
58
 
@@ -125,6 +127,12 @@ After a model change create the migration
125
127
  poetry run aerich migrate
126
128
  ```
127
129
 
130
+ To seed some sample data (attention: drops all current data) use
131
+
132
+ ```bash
133
+ poetry run generate-sample-data
134
+ ```
135
+
128
136
  ### Code formatting and linting
129
137
 
130
138
  Code is formatted using different tools
@@ -147,7 +155,7 @@ poetry run pre-commit install
147
155
  To manually apply the hooks to all files use:
148
156
 
149
157
  ```bash
150
- pre-commit run --all-files
158
+ poetry run pre-commit run --all-files
151
159
  ```
152
160
 
153
161
  ### Testing