goosebit 0.2.3__py3-none-any.whl → 0.2.4__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 (40) hide show
  1. goosebit/__init__.py +16 -3
  2. goosebit/api/v1/devices/device/routes.py +8 -2
  3. goosebit/api/v1/devices/responses.py +0 -7
  4. goosebit/api/v1/devices/routes.py +2 -1
  5. goosebit/api/v1/rollouts/responses.py +2 -7
  6. goosebit/api/v1/rollouts/routes.py +7 -3
  7. goosebit/api/v1/software/responses.py +0 -7
  8. goosebit/api/v1/software/routes.py +24 -11
  9. goosebit/auth/__init__.py +7 -7
  10. goosebit/db/__init__.py +12 -1
  11. goosebit/db/models.py +13 -2
  12. goosebit/schema/devices.py +41 -37
  13. goosebit/schema/rollouts.py +21 -18
  14. goosebit/schema/software.py +24 -19
  15. goosebit/ui/bff/common/__init__.py +0 -0
  16. goosebit/ui/bff/common/requests.py +56 -0
  17. goosebit/ui/bff/common/util.py +32 -0
  18. goosebit/ui/bff/devices/responses.py +12 -20
  19. goosebit/ui/bff/devices/routes.py +9 -6
  20. goosebit/ui/bff/rollouts/responses.py +12 -20
  21. goosebit/ui/bff/rollouts/routes.py +8 -6
  22. goosebit/ui/bff/software/responses.py +19 -19
  23. goosebit/ui/bff/software/routes.py +29 -16
  24. goosebit/ui/nav.py +1 -1
  25. goosebit/ui/routes.py +4 -4
  26. goosebit/ui/static/js/devices.js +135 -25
  27. goosebit/ui/static/js/rollouts.js +4 -0
  28. goosebit/ui/static/js/util.js +23 -14
  29. goosebit/ui/templates/devices.html.jinja +77 -29
  30. goosebit/ui/templates/nav.html.jinja +22 -2
  31. goosebit/ui/templates/rollouts.html.jinja +23 -23
  32. goosebit/updater/controller/v1/routes.py +7 -3
  33. goosebit/updater/controller/v1/schema.py +4 -4
  34. goosebit/updater/manager.py +16 -8
  35. goosebit/updates/__init__.py +14 -21
  36. goosebit/updates/swdesc.py +35 -14
  37. {goosebit-0.2.3.dist-info → goosebit-0.2.4.dist-info}/METADATA +11 -3
  38. {goosebit-0.2.3.dist-info → goosebit-0.2.4.dist-info}/RECORD +40 -37
  39. {goosebit-0.2.3.dist-info → goosebit-0.2.4.dist-info}/LICENSE +0 -0
  40. {goosebit-0.2.3.dist-info → goosebit-0.2.4.dist-info}/WHEEL +0 -0
@@ -28,46 +28,94 @@
28
28
  </div>
29
29
  </div>
30
30
  </div>
31
- <div class="modal" id="device-config-modal">
32
- <div class="modal-dialog modal-lg">
31
+ <div class="modal modal-lg fade" id="device-config-modal">
32
+ <div class="modal-dialog modal-dialog-centered modal-xl">
33
33
  <div class="modal-content">
34
34
  <div class="modal-header">
35
- <h5 class="modal-title">Configure Devices</h5>
35
+ <h5 class="modal-title">Edit Devices</h5>
36
36
  <button type="button"
37
37
  class="btn-close"
38
38
  data-bs-dismiss="modal"
39
39
  aria-label="Close"></button>
40
40
  </div>
41
- <form id="device-config-form" class="needs-validation" novalidate>
42
- <div class="modal-body">
43
- <div class="form-group mb-3">
44
- <label for="device-selected-name">Name</label>
45
- <input id="device-selected-name" class="form-control" placeholder="Name" />
41
+ <div class="modal-body">
42
+ <form id="device-name-form">
43
+ <div class="input-group mb-3">
44
+ <span class="input-group-text">Name</span>
45
+ <input id="device-name" class="form-control" />
46
+ <button type="submit" class="btn btn-outline-light">Apply</button>
46
47
  </div>
47
- <div class="form-group mb-3">
48
- <label for="device-selected-feed">Feed</label>
49
- <input id="device-selected-feed"
50
- class="form-control"
51
- placeholder="Feed"
52
- required />
53
- <div class="invalid-feedback">
54
- Feed missing. Use "default" if working with a single
55
- feed.
56
- </div>
48
+ </form>
49
+ <hr>
50
+ <ul class="nav nav-underline nav-justified w-100" role="tablist">
51
+ <li class="nav-item">
52
+ <button class="nav-link active"
53
+ aria-current="page"
54
+ id="rollout-tab"
55
+ data-bs-toggle="tab"
56
+ data-bs-target="#rollout-tab-content"
57
+ type="button"
58
+ role="tab">Software Rollout</button>
59
+ </li>
60
+ <li class="nav-item">
61
+ <button class="nav-link"
62
+ id="manual-tab"
63
+ data-bs-toggle="tab"
64
+ data-bs-target="#manual-tab-content"
65
+ type="button"
66
+ role="tab">Manual Software</button>
67
+ </li>
68
+ <li class="nav-item">
69
+ <button class="nav-link"
70
+ id="latest-tab"
71
+ data-bs-toggle="tab"
72
+ data-bs-target="#latest-tab-content"
73
+ type="button"
74
+ role="tab">Latest Software</button>
75
+ </li>
76
+ </ul>
77
+ <div class="tab-content mt-3">
78
+ <div class="tab-pane active" id="rollout-tab-content">
79
+ <form id="device-software-rollout-form" class="needs-validation" novalidate>
80
+ <div class="form-group mb-3">
81
+ <div class="input-group mb-3 has-validation">
82
+ <span class="input-group-text">Feed</span>
83
+ <input id="device-selected-feed"
84
+ class="form-control"
85
+ value="default"
86
+ required />
87
+ <div class="invalid-feedback">Feed missing. Use "default" if working with a single feed.</div>
88
+ </div>
89
+ </div>
90
+ <button type="submit" class="btn btn-outline-light w-100">Use Software Rollout</button>
91
+ </form>
57
92
  </div>
58
- <div class="form-group mb-3">
59
- <label for="selected-sw">Update Mode</label>
60
- <select class="form-select" id="selected-sw" required>
61
- <option value="" disabled selected>Select software</option>
62
- </select>
63
- <div class="invalid-feedback">Mode missing.</div>
93
+ <div class="tab-pane" id="manual-tab-content">
94
+ <form id="device-software-manual-form" class="needs-validation" novalidate>
95
+ <div class="form-group mb-3">
96
+ <div class="input-group mb-3 has-validation">
97
+ <span class="input-group-text">Software</span>
98
+ <select class="form-control"
99
+ id="selected-sw"
100
+ data-size="5"
101
+ data-style-base="form-control"
102
+ title="Select software"
103
+ data-live-search="true"
104
+ data-live-search-normalize="true"
105
+ required></select>
106
+ <div class="invalid-feedback">Software missing.</div>
107
+ </div>
108
+ </div>
109
+ <button type="submit" class="btn btn-outline-light w-100">Use Manual Software</button>
110
+ </form>
111
+ </div>
112
+ <div class="tab-pane" id="latest-tab-content">
113
+ <form id="device-software-latest-form" class="needs-validation" novalidate>
114
+ <button type="submit" class="btn btn-outline-light w-100">Use Latest Software</button>
115
+ </form>
64
116
  </div>
65
117
  </div>
66
- <div class="modal-footer">
67
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
68
- <button type="submit" class="btn btn-outline-light">Save changes</button>
69
- </div>
70
- </form>
118
+ </div>
71
119
  </div>
72
120
  </div>
73
121
  </div>
@@ -10,8 +10,11 @@
10
10
  rel="stylesheet"
11
11
  integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
12
12
  crossorigin="anonymous" />
13
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
14
- integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
13
+ <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
14
+ integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
15
+ crossorigin="anonymous"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
17
+ integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
15
18
  crossorigin="anonymous"></script>
16
19
  <link rel="stylesheet"
17
20
  href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
@@ -27,6 +30,10 @@
27
30
  <link href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css"
28
31
  rel="stylesheet" />
29
32
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.all.min.js"></script>
33
+ <!-- Bootstrap select (searchable select) -->
34
+ <link rel="stylesheet"
35
+ href="https://cdn.jsdelivr.net/npm/bootstrap-select@1.14.0-beta3/dist/css/bootstrap-select.min.css">
36
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap-select@1.14.0-beta3/dist/js/bootstrap-select.min.js"></script>
30
37
  <!--data tables alignment fix-->
31
38
  <style>
32
39
  th.dt-type-numeric {
@@ -40,6 +47,19 @@
40
47
  background-color: transparent;
41
48
  }
42
49
  </style>
50
+ <!--select search fix-->
51
+ <style>
52
+ .no-results {
53
+ background-color: var(--bs-body-bg)!important;
54
+ }
55
+ .bs-searchbox input {
56
+ border: var(--bs-border-width) solid var(--bs-border-color)!important;
57
+ }
58
+ .was-validated .bs-searchbox input {
59
+ background-image: none!important;
60
+ box-shadow: none!important;
61
+ }
62
+ </style>
43
63
  <script>const TABLE_UPDATE_TIME = 3000;</script>
44
64
  <script src="{{ url_for('static', path='js/util.js') }}"></script>
45
65
  </head>
@@ -36,35 +36,35 @@
36
36
  </div>
37
37
  <form id="rollout-form" class="needs-validation" novalidate>
38
38
  <div class="modal-body">
39
- <div class="form-group mb-3">
40
- <label for="rollout-selected-name">Name</label>
41
- <input id="rollout-selected-name"
42
- class="form-control"
43
- placeholder="Release 1" />
39
+ <div class="input-group mb-3 has-validation">
40
+ <span class="input-group-text">Name</span>
41
+ <input class="form-control" id="rollout-selected-name" required />
42
+ <div class="invalid-feedback">Name missing.</div>
43
+ </div>
44
+ <div class="input-group mb-3 has-validation">
45
+ <span class="input-group-text">Feed</span>
46
+ <input class="form-control" id="rollout-selected-feed" required />
47
+ <div class="invalid-feedback">Feed missing. Use "default" if working with a single feed.</div>
44
48
  </div>
45
49
  <div class="form-group mb-3">
46
- <label for="rollout-selected-feed">Feed</label>
47
- <input id="rollout-selected-feed"
48
- class="form-control"
49
- placeholder="qa"
50
- required />
51
- <div class="invalid-feedback">
52
- Feed missing. Use "default" if working with a single
53
- feed.
50
+ <div class="input-group mb-3 has-validation">
51
+ <span class="input-group-text">Software</span>
52
+ <select class="form-control"
53
+ id="selected-sw"
54
+ data-size="5"
55
+ data-style-base="form-control"
56
+ title="Select software"
57
+ data-live-search="true"
58
+ data-live-search-normalize="true"
59
+ required></select>
60
+ <div class="invalid-feedback">Software missing.</div>
54
61
  </div>
55
62
  </div>
56
- <div class="form-group mb-3">
57
- <label for="selected-sw">Software</label>
58
- <select class="form-select" id="selected-sw" required>
59
- <option value="" disabled selected>Select software</option>
60
- </select>
61
- <div class="invalid-feedback">Select software for the rollout.</div>
63
+ <div class="modal-footer">
64
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
65
+ <button type="submit" class="btn btn-outline-light">Save changes</button>
62
66
  </div>
63
67
  </div>
64
- <div class="modal-footer">
65
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
66
- <button type="submit" class="btn btn-outline-light">Save changes</button>
67
- </div>
68
68
  </form>
69
69
  </div>
70
70
  </div>
@@ -23,11 +23,14 @@ router = APIRouter(prefix="/v1")
23
23
 
24
24
  @router.get("/{dev_id}")
25
25
  async def polling(request: Request, dev_id: str, updater: UpdateManager = Depends(get_update_manager)):
26
- links = {}
26
+ links: dict[str, dict[str, str]] = {}
27
27
 
28
28
  sleep = updater.poll_time
29
29
  device = await updater.get_device()
30
30
 
31
+ if device is None:
32
+ raise HTTPException(404)
33
+
31
34
  if device.last_state == UpdateStateEnum.UNKNOWN:
32
35
  # device registration
33
36
  sleep = config.poll_time_registration
@@ -49,7 +52,7 @@ async def polling(request: Request, dev_id: str, updater: UpdateManager = Depend
49
52
  # provide update if available. Note: this is also required while in state "running", otherwise swupdate
50
53
  # won't confirm a successful testing (might be a bug/problem in swupdate)
51
54
  handling_type, software = await updater.get_update()
52
- if handling_type != HandlingType.SKIP:
55
+ if handling_type != HandlingType.SKIP and software is not None:
53
56
  links["deploymentBase"] = {
54
57
  "href": str(
55
58
  request.url_for(
@@ -152,7 +155,8 @@ async def deployment_feedback(
152
155
 
153
156
  try:
154
157
  log = data.status.details
155
- await updater.update_log("\n".join(log))
158
+ if log is not None:
159
+ await updater.update_log("\n".join(log))
156
160
  except AttributeError:
157
161
  logging.warning(f"No details to update device update log, device={updater.dev_id}")
158
162
 
@@ -41,16 +41,16 @@ class FeedbackStatusResultFinished(StrEnum):
41
41
 
42
42
  class FeedbackStatusResultSchema(BaseModel):
43
43
  finished: FeedbackStatusResultFinished
44
- progress: FeedbackStatusProgressSchema = None
44
+ progress: FeedbackStatusProgressSchema | None = None
45
45
 
46
46
 
47
47
  class FeedbackStatusSchema(BaseModel):
48
48
  execution: FeedbackStatusExecutionState
49
49
  result: FeedbackStatusResultSchema
50
- code: int = None
51
- details: list[str] = None
50
+ code: int | None = None
51
+ details: list[str] | None = None
52
52
 
53
53
 
54
54
  class FeedbackSchema(BaseModel):
55
- time: str = None
55
+ time: str | None = None
56
56
  status: FeedbackStatusSchema
@@ -45,7 +45,7 @@ class UpdateManager(ABC):
45
45
  self.dev_id = dev_id
46
46
 
47
47
  async def get_device(self) -> Device | None:
48
- return
48
+ return None
49
49
 
50
50
  async def update_force_update(self, force_update: bool) -> None:
51
51
  return
@@ -86,7 +86,8 @@ class UpdateManager(ABC):
86
86
  subscribers = self.log_subscribers
87
87
  subscribers.append(callback)
88
88
  self.log_subscribers = subscribers
89
- await callback(device.last_log)
89
+ if device is not None:
90
+ await callback(device.last_log)
90
91
  try:
91
92
  yield
92
93
  except asyncio.CancelledError:
@@ -126,7 +127,7 @@ class UpdateManager(ABC):
126
127
  await cb(log_data)
127
128
 
128
129
  @abstractmethod
129
- async def get_update(self) -> tuple[HandlingType, Software]: ...
130
+ async def get_update(self) -> tuple[HandlingType, Software | None]: ...
130
131
 
131
132
  @abstractmethod
132
133
  async def update_log(self, log_data: str) -> None: ...
@@ -137,11 +138,16 @@ class UnknownUpdateManager(UpdateManager):
137
138
  super().__init__(dev_id)
138
139
  self.poll_time = config.poll_time_updating
139
140
 
140
- async def _get_software(self) -> Software:
141
- return await Software.latest(await self.get_device())
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)
142
146
 
143
- async def get_update(self) -> tuple[HandlingType, Software]:
147
+ async def get_update(self) -> tuple[HandlingType, Software | None]:
144
148
  software = await self._get_software()
149
+ if software is None:
150
+ return HandlingType.SKIP, None
145
151
  return HandlingType.FORCED, software
146
152
 
147
153
  async def update_log(self, log_data: str) -> None:
@@ -161,9 +167,11 @@ class DeviceUpdateManager(UpdateManager):
161
167
  return (await Device.get_or_create(uuid=self.dev_id, defaults={"hardware": hardware}))[0]
162
168
 
163
169
  async def save_device(self, device: Device, update_fields: list[str]):
170
+ await device.save(update_fields=update_fields)
171
+
172
+ # only update cache after a successful database save
164
173
  result = await caches.get("default").set(self.dev_id, device, ttl=600)
165
174
  assert result, "device being cached"
166
- await device.save(update_fields=update_fields)
167
175
 
168
176
  async def update_force_update(self, force_update: bool) -> None:
169
177
  device = await self.get_device()
@@ -276,7 +284,7 @@ class DeviceUpdateManager(UpdateManager):
276
284
  assert device.update_mode == UpdateModeEnum.PINNED
277
285
  return None
278
286
 
279
- async def get_update(self) -> tuple[HandlingType, Software]:
287
+ async def get_update(self) -> tuple[HandlingType, Software | None]:
280
288
  device = await self.get_device()
281
289
  software = await self._get_software()
282
290
 
@@ -1,8 +1,9 @@
1
- import shutil
2
- from pathlib import Path
1
+ from __future__ import annotations
2
+
3
3
  from urllib.parse import unquote, urlparse
4
4
  from urllib.request import url2pathname
5
5
 
6
+ from anyio import Path
6
7
  from fastapi import HTTPException
7
8
  from fastapi.requests import Request
8
9
  from tortoise.expressions import Q
@@ -10,6 +11,7 @@ from tortoise.expressions import Q
10
11
  from goosebit.db.models import Hardware, Software
11
12
  from goosebit.updater.manager import UpdateManager
12
13
 
14
+ from ..settings import config
13
15
  from . import swdesc
14
16
 
15
17
 
@@ -18,6 +20,8 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:
18
20
 
19
21
  # parse swu header into update_info
20
22
  if parsed_uri.scheme == "file":
23
+ if temp_file is None:
24
+ raise HTTPException(500, "Temporary file missing, cannot parse file information")
21
25
  try:
22
26
  update_info = await swdesc.parse_file(temp_file)
23
27
  except Exception:
@@ -28,7 +32,6 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:
28
32
  update_info = await swdesc.parse_remote(uri)
29
33
  except Exception:
30
34
  raise HTTPException(422, "Software swu header cannot be parsed")
31
-
32
35
  else:
33
36
  raise HTTPException(422, "Software URI protocol unknown")
34
37
 
@@ -42,10 +45,14 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:
42
45
 
43
46
  # for local file: rename temp file to final name
44
47
  if parsed_uri.scheme == "file":
45
- path = _unique_path(parsed_uri)
46
- path.parent.mkdir(parents=True, exist_ok=True)
47
- shutil.copy(temp_file, path)
48
- uri = path.absolute().as_uri()
48
+ if temp_file is None:
49
+ raise HTTPException(500, "Temporary file missing, cannot parse file information")
50
+ filename = Path(url2pathname(unquote(parsed_uri.path))).name
51
+ path = Path(config.artifacts_dir).joinpath(update_info["hash"], filename)
52
+ await path.parent.mkdir(parents=True, exist_ok=True)
53
+ await temp_file.replace(path)
54
+ absolute = await path.absolute()
55
+ uri = absolute.as_uri()
49
56
 
50
57
  # create software
51
58
  software = await Software.create(
@@ -85,20 +92,6 @@ async def _is_software_colliding(update_info):
85
92
  return is_colliding
86
93
 
87
94
 
88
- def _unique_path(uri):
89
- path = Path(url2pathname(unquote(uri.path)))
90
- if not path.exists():
91
- return path
92
-
93
- counter = 1
94
- new_path = path.with_name(f"{path.stem}-{counter}{path.suffix}")
95
- while new_path.exists():
96
- counter += 1
97
- new_path = path.with_name(f"{path.stem}-{counter}{path.suffix}")
98
-
99
- return new_path
100
-
101
-
102
95
  async def generate_chunk(request: Request, updater: UpdateManager) -> list:
103
96
  _, software = await updater.get_update()
104
97
  if software is None:
@@ -1,12 +1,15 @@
1
1
  import hashlib
2
2
  import logging
3
- from pathlib import Path
3
+ import random
4
+ import string
4
5
  from typing import Any
5
6
 
6
- import aiofiles
7
7
  import httpx
8
8
  import libconf
9
9
  import semver
10
+ from anyio import AsyncFile, Path, open_file
11
+
12
+ from goosebit.settings import config
10
13
 
11
14
  logger = logging.getLogger(__name__)
12
15
 
@@ -21,7 +24,7 @@ def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
21
24
  swdesc_attrs = {}
22
25
  try:
23
26
  swdesc_attrs["version"] = semver.Version.parse(swdesc["software"]["version"])
24
- compatibility = []
27
+ compatibility: list[dict[str, str]] = []
25
28
  _append_compatibility("default", swdesc["software"], compatibility)
26
29
 
27
30
  for key in swdesc["software"]:
@@ -41,7 +44,7 @@ def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
41
44
 
42
45
 
43
46
  async def parse_file(file: Path):
44
- async with aiofiles.open(file, "r+b") as f:
47
+ async with await open_file(file, "r+b") as f:
45
48
  # get file size
46
49
  size = int((await f.read(110))[54:62], 16)
47
50
  filename = b""
@@ -59,20 +62,38 @@ async def parse_file(file: Path):
59
62
  swdesc = libconf.loads((await f.read(size)).decode("utf-8"))
60
63
 
61
64
  swdesc_attrs = parse_descriptor(swdesc)
62
- swdesc_attrs["size"] = file.stat().st_size
63
- swdesc_attrs["hash"] = _sha1_hash_file(file)
65
+ stat = await file.stat()
66
+ swdesc_attrs["size"] = stat.st_size
67
+ swdesc_attrs["hash"] = await _sha1_hash_file(f)
64
68
  return swdesc_attrs
65
69
 
66
70
 
67
71
  async def parse_remote(url: str):
68
72
  async with httpx.AsyncClient() as c:
69
73
  file = await c.get(url)
70
- async with aiofiles.tempfile.NamedTemporaryFile("w+b") as f:
71
- await f.write(file.content)
72
- return await parse_file(Path(f.name))
73
-
74
-
75
- def _sha1_hash_file(file_path: Path):
76
- with file_path.open("rb") as f:
77
- sha1_hash = hashlib.file_digest(f, "sha1")
74
+ artifacts_dir = Path(config.artifacts_dir)
75
+ tmp_file_path = artifacts_dir.joinpath("tmp", ("".join(random.choices(string.ascii_lowercase, k=12)) + ".tmp"))
76
+ await tmp_file_path.parent.mkdir(parents=True, exist_ok=True)
77
+ try:
78
+ async with await open_file(tmp_file_path, "w+b") as f:
79
+ await f.write(file.content)
80
+ file_data = await parse_file(Path(str(f.name)))
81
+ finally:
82
+ await tmp_file_path.unlink()
83
+ return file_data
84
+
85
+
86
+ async def _sha1_hash_file(fileobj: AsyncFile):
87
+ last = await fileobj.tell()
88
+ await fileobj.seek(0)
89
+ sha1_hash = hashlib.sha1()
90
+ buf = bytearray(2**18)
91
+ view = memoryview(buf)
92
+ while True:
93
+ size = await fileobj.readinto(buf)
94
+ if size == 0:
95
+ break
96
+ sha1_hash.update(view[:size])
97
+
98
+ await fileobj.seek(last)
78
99
  return sha1_hash.hexdigest()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goosebit
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary:
5
5
  Author: Upstream Data
6
6
  Author-email: brett@upstreamdata.ca
@@ -11,7 +11,6 @@ Classifier: Programming Language :: Python :: 3.12
11
11
  Provides-Extra: postgresql
12
12
  Requires-Dist: aerich (>=0.7.2,<0.8.0)
13
13
  Requires-Dist: aiocache (>=0.12.2,<0.13.0)
14
- Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
15
14
  Requires-Dist: argon2-cffi (>=23.1.0,<24.0.0)
16
15
  Requires-Dist: asyncpg (>=0.29.0,<0.30.0) ; extra == "postgresql"
17
16
  Requires-Dist: fastapi[uvicorn] (>=0.111.0,<0.112.0)
@@ -43,10 +42,19 @@ A simplistic, opinionated remote update server implementing hawkBit™'s [DDI AP
43
42
  ### Installation
44
43
 
45
44
  1. Install dependencies using [Poetry](https://python-poetry.org/):
45
+
46
46
  ```bash
47
47
  poetry install
48
48
  ```
49
- 2. Launch gooseBit:
49
+
50
+ 2. Create the database:
51
+
52
+ ```bash
53
+ poetry run aerich init -t goosebit.db.config
54
+ poetry run aerich upgrade
55
+ ```
56
+
57
+ 3. Launch gooseBit:
50
58
  ```bash
51
59
  python main.py
52
60
  ```