goosebit 0.2.5__py3-none-any.whl → 0.2.6__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.
- goosebit/__init__.py +41 -7
- goosebit/api/telemetry/metrics.py +1 -5
- goosebit/api/v1/devices/device/responses.py +1 -0
- goosebit/api/v1/devices/device/routes.py +8 -8
- goosebit/api/v1/devices/requests.py +20 -0
- goosebit/api/v1/devices/routes.py +68 -8
- goosebit/api/v1/download/routes.py +14 -3
- goosebit/api/v1/rollouts/routes.py +5 -4
- goosebit/api/v1/routes.py +2 -1
- goosebit/api/v1/settings/routes.py +14 -0
- goosebit/api/v1/settings/users/__init__.py +1 -0
- goosebit/api/v1/settings/users/requests.py +16 -0
- goosebit/api/v1/settings/users/responses.py +7 -0
- goosebit/api/v1/settings/users/routes.py +56 -0
- goosebit/api/v1/software/routes.py +18 -14
- goosebit/auth/__init__.py +49 -13
- goosebit/auth/permissions.py +80 -0
- goosebit/db/config.py +57 -1
- goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
- goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
- goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
- goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
- goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
- goosebit/db/models.py +19 -8
- goosebit/db/pg_ssl_context.py +51 -0
- goosebit/device_manager.py +262 -0
- goosebit/plugins/__init__.py +32 -0
- goosebit/schema/devices.py +8 -5
- goosebit/schema/plugins.py +67 -0
- goosebit/schema/updates.py +15 -0
- goosebit/schema/users.py +9 -0
- goosebit/settings/__init__.py +0 -3
- goosebit/settings/schema.py +60 -14
- goosebit/storage/__init__.py +62 -0
- goosebit/storage/base.py +14 -0
- goosebit/storage/filesystem.py +111 -0
- goosebit/storage/s3.py +104 -0
- goosebit/ui/bff/common/columns.py +50 -0
- goosebit/ui/bff/common/responses.py +1 -0
- goosebit/ui/bff/devices/device/__init__.py +1 -0
- goosebit/ui/bff/devices/device/routes.py +17 -0
- goosebit/ui/bff/devices/requests.py +1 -0
- goosebit/ui/bff/devices/routes.py +49 -46
- goosebit/ui/bff/download/routes.py +14 -3
- goosebit/ui/bff/rollouts/routes.py +32 -4
- goosebit/ui/bff/routes.py +2 -1
- goosebit/ui/bff/settings/__init__.py +1 -0
- goosebit/ui/bff/settings/routes.py +20 -0
- goosebit/ui/bff/settings/users/__init__.py +1 -0
- goosebit/ui/bff/settings/users/responses.py +33 -0
- goosebit/ui/bff/settings/users/routes.py +80 -0
- goosebit/ui/bff/software/routes.py +40 -12
- goosebit/ui/nav.py +12 -2
- goosebit/ui/routes.py +66 -13
- goosebit/ui/static/js/devices.js +32 -24
- goosebit/ui/static/js/login.js +21 -5
- goosebit/ui/static/js/logs.js +7 -22
- goosebit/ui/static/js/rollouts.js +31 -30
- goosebit/ui/static/js/settings.js +322 -0
- goosebit/ui/static/js/setup.js +28 -0
- goosebit/ui/static/js/software.js +127 -121
- goosebit/ui/static/js/util.js +25 -4
- goosebit/ui/templates/__init__.py +10 -1
- goosebit/ui/templates/login.html.jinja +5 -0
- goosebit/ui/templates/nav.html.jinja +13 -5
- goosebit/ui/templates/rollouts.html.jinja +4 -22
- goosebit/ui/templates/settings.html.jinja +88 -0
- goosebit/ui/templates/setup.html.jinja +71 -0
- goosebit/ui/templates/software.html.jinja +0 -11
- goosebit/updater/controller/v1/routes.py +119 -77
- goosebit/updater/routes.py +83 -8
- goosebit/updates/__init__.py +24 -31
- goosebit/updates/swdesc.py +15 -8
- goosebit/users/__init__.py +63 -0
- goosebit/util/__init__.py +0 -0
- goosebit/util/path.py +42 -0
- goosebit/util/version.py +92 -0
- goosebit-0.2.6.dist-info/METADATA +280 -0
- goosebit-0.2.6.dist-info/RECORD +133 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.6.dist-info}/WHEEL +1 -1
- goosebit/realtime/logs.py +0 -42
- goosebit/realtime/routes.py +0 -13
- goosebit/updater/manager.py +0 -325
- goosebit-0.2.5.dist-info/METADATA +0 -189
- goosebit-0.2.5.dist-info/RECORD +0 -99
- /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.6.dist-info}/LICENSE +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.6.dist-info}/entry_points.txt +0 -0
goosebit/updater/routes.py
CHANGED
@@ -1,25 +1,100 @@
|
|
1
1
|
import time
|
2
2
|
|
3
|
-
|
3
|
+
import httpx
|
4
|
+
from fastapi import APIRouter, Depends, HTTPException
|
4
5
|
from fastapi.requests import Request
|
5
6
|
|
7
|
+
from goosebit.device_manager import DeviceManager, get_device_or_none
|
6
8
|
from goosebit.settings import config
|
9
|
+
from goosebit.settings.schema import DeviceAuthMode, ExternalAuthMode
|
7
10
|
|
11
|
+
from ..db import Device
|
8
12
|
from . import controller
|
9
|
-
from .manager import get_update_manager
|
10
13
|
|
11
14
|
|
12
15
|
async def log_last_connection(request: Request, dev_id: str):
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
+
device = await get_device_or_none(dev_id)
|
17
|
+
|
18
|
+
if not device:
|
19
|
+
return
|
20
|
+
|
21
|
+
if request.scope["config"].track_device_ip:
|
22
|
+
await DeviceManager.update_last_connection(device, round(time.time()), request.client.host)
|
16
23
|
else:
|
17
|
-
await
|
24
|
+
await DeviceManager.update_last_connection(device, round(time.time()))
|
25
|
+
|
26
|
+
|
27
|
+
async def validate_device_token(request: Request, dev_id: str):
|
28
|
+
if not request.scope["config"].device_auth.enable:
|
29
|
+
return
|
30
|
+
|
31
|
+
# parse device token, needs to be the `TargetToken`
|
32
|
+
device_token = request.headers.get("Authorization")
|
33
|
+
if device_token is not None:
|
34
|
+
if device_token.startswith("TargetToken"):
|
35
|
+
device_token = device_token.replace("TargetToken ", "")
|
36
|
+
else:
|
37
|
+
device_token = None
|
38
|
+
|
39
|
+
# setup mode should register devices and set up their auth token
|
40
|
+
if request.scope["config"].device_auth.mode == DeviceAuthMode.SETUP:
|
41
|
+
device = await DeviceManager.get_device(dev_id)
|
42
|
+
if device_token is None:
|
43
|
+
return
|
44
|
+
await DeviceManager.update_auth_token(device, device_token)
|
45
|
+
|
46
|
+
# lax mode should register devices and check their token if they have one, but not register their tokens
|
47
|
+
elif request.scope["config"].device_auth.mode == DeviceAuthMode.LAX:
|
48
|
+
device = await DeviceManager.get_device(dev_id)
|
49
|
+
# should not be possible
|
50
|
+
assert device is not None
|
51
|
+
|
52
|
+
if not device.auth_token == device_token:
|
53
|
+
raise HTTPException(401, "Device authentication token does not match.")
|
54
|
+
|
55
|
+
# strict mode should ensure all device are already set up and have a token, then check the token
|
56
|
+
elif request.scope["config"].device_auth.mode == DeviceAuthMode.STRICT:
|
57
|
+
if device_token is None:
|
58
|
+
raise HTTPException(401, "Device authentication token is required in strict mode.")
|
59
|
+
# do not create a device in strict mode
|
60
|
+
device = await Device.get_or_none(id=dev_id)
|
61
|
+
if device is None:
|
62
|
+
raise HTTPException(401, "Cannot register a new device in strict mode.")
|
63
|
+
if not device.auth_token == device_token:
|
64
|
+
raise HTTPException(401, "Device authentication token does not match.")
|
65
|
+
|
66
|
+
# external mode should check the token with an external service
|
67
|
+
elif request.scope["config"].device_auth.mode == DeviceAuthMode.EXTERNAL:
|
68
|
+
if device_token is None:
|
69
|
+
raise HTTPException(401, "Device authentication token is required in external mode.")
|
70
|
+
|
71
|
+
try:
|
72
|
+
async with httpx.AsyncClient() as client:
|
73
|
+
if request.scope["config"].device_auth.external_mode == ExternalAuthMode.BEARER:
|
74
|
+
response = await client.post(
|
75
|
+
request.scope["config"].device_auth.external_url,
|
76
|
+
headers={"Authorization": f"Bearer {device_token}"},
|
77
|
+
)
|
78
|
+
elif request.scope["config"].device_auth.external_mode == ExternalAuthMode.JSON:
|
79
|
+
json = {request.scope["config"].device_auth.external_json_key: device_token}
|
80
|
+
response = await client.post(
|
81
|
+
request.scope["config"].device_auth.external_url,
|
82
|
+
json=json,
|
83
|
+
)
|
84
|
+
|
85
|
+
if response.status_code != 200:
|
86
|
+
raise HTTPException(401, "Device authentication token is invalid.")
|
87
|
+
else:
|
88
|
+
await DeviceManager.get_device(dev_id)
|
89
|
+
except httpx.RequestError as e:
|
90
|
+
raise HTTPException(401, f"Error communicating with authentication service: {str(e)}")
|
91
|
+
except Exception as e:
|
92
|
+
raise HTTPException(401, f"Error: {str(e)}")
|
18
93
|
|
19
94
|
|
20
95
|
router = APIRouter(
|
21
|
-
prefix="/
|
22
|
-
dependencies=[Depends(log_last_connection)],
|
96
|
+
prefix=f"/{config.tenant}",
|
97
|
+
dependencies=[Depends(log_last_connection), Depends(validate_device_token)],
|
23
98
|
tags=["ddi"],
|
24
99
|
)
|
25
100
|
router.include_router(controller.router)
|
goosebit/updates/__init__.py
CHANGED
@@ -8,10 +8,11 @@ from fastapi import HTTPException
|
|
8
8
|
from fastapi.requests import Request
|
9
9
|
from tortoise.expressions import Q
|
10
10
|
|
11
|
-
from goosebit.db.models import Hardware, Software
|
12
|
-
from goosebit.
|
11
|
+
from goosebit.db.models import Device, Hardware, Software
|
12
|
+
from goosebit.device_manager import DeviceManager
|
13
|
+
from goosebit.schema.updates import UpdateChunk, UpdateChunkArtifact
|
14
|
+
from goosebit.storage import storage
|
13
15
|
|
14
|
-
from ..settings import config
|
15
16
|
from . import swdesc
|
16
17
|
|
17
18
|
|
@@ -48,11 +49,9 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:
|
|
48
49
|
if temp_file is None:
|
49
50
|
raise HTTPException(500, "Temporary file missing, cannot parse file information")
|
50
51
|
filename = Path(url2pathname(unquote(parsed_uri.path))).name
|
51
|
-
|
52
|
-
|
53
|
-
await
|
54
|
-
absolute = await path.absolute()
|
55
|
-
uri = absolute.as_uri()
|
52
|
+
|
53
|
+
dest_path = Path(update_info["hash"]).joinpath(filename)
|
54
|
+
uri = await storage.store_file(temp_file, dest_path)
|
56
55
|
|
57
56
|
# create software
|
58
57
|
software = await Software.create(
|
@@ -92,31 +91,25 @@ async def _is_software_colliding(update_info):
|
|
92
91
|
return is_colliding
|
93
92
|
|
94
93
|
|
95
|
-
async def generate_chunk(request: Request,
|
96
|
-
_, software = await
|
94
|
+
async def generate_chunk(request: Request, device: Device) -> list[UpdateChunk]:
|
95
|
+
_, software = await DeviceManager.get_update(device)
|
97
96
|
if software is None:
|
98
97
|
return []
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
)
|
105
|
-
)
|
106
|
-
else:
|
107
|
-
href = software.uri
|
98
|
+
|
99
|
+
# Always use the download endpoint for consistency, the endpoint
|
100
|
+
# will handle both local and remote files appropriately.
|
101
|
+
href = str(request.url_for("download_artifact", dev_id=device.id))
|
102
|
+
|
108
103
|
return [
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
"
|
117
|
-
|
118
|
-
"_links": {"download": {"href": href}},
|
119
|
-
}
|
104
|
+
UpdateChunk(
|
105
|
+
name=software.path.name,
|
106
|
+
artifacts=[
|
107
|
+
UpdateChunkArtifact(
|
108
|
+
filename=software.path.name,
|
109
|
+
hashes={"sha1": software.hash},
|
110
|
+
size=software.size,
|
111
|
+
links={"download": {"href": href}},
|
112
|
+
)
|
120
113
|
],
|
121
|
-
|
114
|
+
)
|
122
115
|
]
|
goosebit/updates/swdesc.py
CHANGED
@@ -6,15 +6,17 @@ from typing import Any
|
|
6
6
|
|
7
7
|
import httpx
|
8
8
|
import libconf
|
9
|
-
import semver
|
10
9
|
from anyio import AsyncFile, Path, open_file
|
11
10
|
|
12
|
-
from goosebit.
|
11
|
+
from goosebit.storage import storage
|
12
|
+
from goosebit.util.version import Version
|
13
13
|
|
14
14
|
logger = logging.getLogger(__name__)
|
15
15
|
|
16
16
|
|
17
17
|
def _append_compatibility(boardname, value, compatibility):
|
18
|
+
if not isinstance(value, dict):
|
19
|
+
return
|
18
20
|
if "hardware-compatibility" in value:
|
19
21
|
for revision in value["hardware-compatibility"]:
|
20
22
|
compatibility.append({"hw_model": boardname, "hw_revision": revision})
|
@@ -23,7 +25,7 @@ def _append_compatibility(boardname, value, compatibility):
|
|
23
25
|
def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
|
24
26
|
swdesc_attrs = {}
|
25
27
|
try:
|
26
|
-
swdesc_attrs["version"] =
|
28
|
+
swdesc_attrs["version"] = Version.parse(swdesc["software"]["version"])
|
27
29
|
compatibility: list[dict[str, str]] = []
|
28
30
|
_append_compatibility("default", swdesc["software"], compatibility)
|
29
31
|
|
@@ -35,6 +37,10 @@ def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
|
|
35
37
|
for key2 in element:
|
36
38
|
_append_compatibility(key, element[key2], compatibility)
|
37
39
|
|
40
|
+
if len(compatibility) == 0:
|
41
|
+
# if nothing is specified, assume compatibility with default / default boards
|
42
|
+
compatibility.append({"hw_model": "default", "hw_revision": "default"})
|
43
|
+
|
38
44
|
swdesc_attrs["compatibility"] = compatibility
|
39
45
|
except KeyError as e:
|
40
46
|
logging.warning(f"Parsing swu descriptor failed, error={e}")
|
@@ -71,15 +77,16 @@ async def parse_file(file: Path):
|
|
71
77
|
async def parse_remote(url: str):
|
72
78
|
async with httpx.AsyncClient() as c:
|
73
79
|
file = await c.get(url)
|
74
|
-
|
75
|
-
tmp_file_path =
|
76
|
-
await tmp_file_path.parent.mkdir(parents=True, exist_ok=True)
|
80
|
+
temp_dir = Path(storage.get_temp_dir())
|
81
|
+
tmp_file_path = temp_dir.joinpath("".join(random.choices(string.ascii_lowercase, k=12)) + ".tmp")
|
77
82
|
try:
|
78
83
|
async with await open_file(tmp_file_path, "w+b") as f:
|
79
84
|
await f.write(file.content)
|
80
|
-
file_data = await parse_file(Path
|
85
|
+
file_data = await parse_file(tmp_file_path) # Use anyio.Path for parse_file
|
86
|
+
except Exception:
|
87
|
+
raise
|
81
88
|
finally:
|
82
|
-
await tmp_file_path.unlink()
|
89
|
+
await tmp_file_path.unlink(missing_ok=True)
|
83
90
|
return file_data
|
84
91
|
|
85
92
|
|
@@ -0,0 +1,63 @@
|
|
1
|
+
from aiocache import caches
|
2
|
+
|
3
|
+
from goosebit.api.telemetry.metrics import users_count
|
4
|
+
from goosebit.db.models import User
|
5
|
+
from goosebit.settings import PWD_CXT
|
6
|
+
|
7
|
+
|
8
|
+
async def create_user(username: str, password: str, permissions: list[str]) -> User:
|
9
|
+
return await UserManager.setup_user(username=username, hashed_pwd=PWD_CXT.hash(password), permissions=permissions)
|
10
|
+
|
11
|
+
|
12
|
+
async def create_initial_user(username: str, hashed_pwd: str) -> User:
|
13
|
+
return await UserManager.setup_user(username=username, hashed_pwd=hashed_pwd, permissions=["*"])
|
14
|
+
|
15
|
+
|
16
|
+
class UserManager:
|
17
|
+
@staticmethod
|
18
|
+
async def save_user(user: User, update_fields: list[str]) -> None:
|
19
|
+
await user.save(update_fields=update_fields)
|
20
|
+
|
21
|
+
# only update cache after a successful database save
|
22
|
+
result = await caches.get("default").set(user.username, user, ttl=600)
|
23
|
+
assert result, "user being cached"
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
async def update_enabled(user: User, enabled: bool) -> None:
|
27
|
+
user.enabled = enabled
|
28
|
+
await UserManager.save_user(user, update_fields=["enabled"])
|
29
|
+
|
30
|
+
@classmethod
|
31
|
+
async def setup_user(cls, username: str, hashed_pwd: str, permissions: list[str]) -> User:
|
32
|
+
user = (
|
33
|
+
await User.get_or_create(
|
34
|
+
username=username,
|
35
|
+
defaults={
|
36
|
+
"hashed_pwd": hashed_pwd,
|
37
|
+
"permissions": permissions,
|
38
|
+
},
|
39
|
+
)
|
40
|
+
)[0]
|
41
|
+
users_count.set(await User.all().count())
|
42
|
+
return user
|
43
|
+
|
44
|
+
@staticmethod
|
45
|
+
async def get_user(username: str) -> User:
|
46
|
+
cache = caches.get("default")
|
47
|
+
user = await cache.get(username)
|
48
|
+
if user:
|
49
|
+
return user
|
50
|
+
|
51
|
+
user = await User.get_or_none(username=username)
|
52
|
+
if user is not None:
|
53
|
+
result = await cache.set(user.username, user, ttl=600)
|
54
|
+
assert result, "user being cached"
|
55
|
+
|
56
|
+
return user
|
57
|
+
|
58
|
+
@staticmethod
|
59
|
+
async def delete_users(usernames: list[str]):
|
60
|
+
await User.filter(username__in=usernames).delete()
|
61
|
+
for username in usernames:
|
62
|
+
await caches.get("default").delete(username)
|
63
|
+
users_count.set(await User.all().count())
|
File without changes
|
goosebit/util/path.py
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
from anyio import Path
|
2
|
+
|
3
|
+
|
4
|
+
async def validate_filename(filename: str, temp_dir: Path) -> Path:
|
5
|
+
if not filename or not isinstance(filename, str):
|
6
|
+
raise ValueError("Filename must be a non-empty string")
|
7
|
+
|
8
|
+
filename = filename.strip()
|
9
|
+
if not filename:
|
10
|
+
raise ValueError("Filename cannot be empty or whitespace only")
|
11
|
+
|
12
|
+
# Check for dangerous patterns that could indicate path traversal
|
13
|
+
# This includes both Unix (..) and Windows (..\) style traversal
|
14
|
+
dangerous_patterns = ["../", "..\\", "../", "..\\"]
|
15
|
+
if any(pattern in filename for pattern in dangerous_patterns):
|
16
|
+
raise ValueError("Filename contains invalid path traversal components")
|
17
|
+
|
18
|
+
# Check for Windows drive letters (C:, D:, etc.)
|
19
|
+
if len(filename) >= 2 and filename[1] == ":" and filename[0].isalpha():
|
20
|
+
raise ValueError("Filename cannot contain Windows drive letters")
|
21
|
+
|
22
|
+
# Create a path from the filename
|
23
|
+
filename_path = Path(filename)
|
24
|
+
|
25
|
+
# Check if it's an absolute path
|
26
|
+
if filename_path.is_absolute():
|
27
|
+
raise ValueError("Filename cannot be an absolute path")
|
28
|
+
|
29
|
+
# Construct the full path within temp directory
|
30
|
+
file_path = temp_dir / filename_path
|
31
|
+
|
32
|
+
# Resolve both paths to check for path traversal
|
33
|
+
resolved_file = await file_path.resolve()
|
34
|
+
resolved_temp = await temp_dir.resolve()
|
35
|
+
|
36
|
+
# Ensure the resolved file path is within the temp directory
|
37
|
+
try:
|
38
|
+
resolved_file.relative_to(resolved_temp)
|
39
|
+
except ValueError:
|
40
|
+
raise ValueError("Filename contains invalid path traversal components")
|
41
|
+
|
42
|
+
return file_path
|
goosebit/util/version.py
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
from functools import total_ordering
|
2
|
+
|
3
|
+
from semver import Version as SemVersion
|
4
|
+
|
5
|
+
|
6
|
+
# Class that replicates version handling of swupdate. For reference see
|
7
|
+
# * https://sbabic.github.io/swupdate/sw-description.html#versioning-schemas-in-swupdate
|
8
|
+
# * https://github.com/sbabic/swupdate/blob/60322d5ad668e5603341c5f42b3c51d0a1c60226/core/artifacts_versions.c#L158
|
9
|
+
# * https://github.com/sbabic/swupdate/blob/60322d5ad668e5603341c5f42b3c51d0a1c60226/core/artifacts_versions.c#L217
|
10
|
+
@total_ordering
|
11
|
+
class Version:
|
12
|
+
version_str: str
|
13
|
+
default_version: int | None
|
14
|
+
sem_version: SemVersion
|
15
|
+
|
16
|
+
def __init__(self, version_str: str, default_version: int | None = None, sem_version: SemVersion = None):
|
17
|
+
self.version_str = version_str
|
18
|
+
self.default_version = default_version
|
19
|
+
self.sem_version = sem_version
|
20
|
+
|
21
|
+
@staticmethod
|
22
|
+
def parse(version_str: str):
|
23
|
+
default_version = Version._default_version_to_number(version_str)
|
24
|
+
sem_version = None
|
25
|
+
try:
|
26
|
+
sem_version = SemVersion.parse(version_str, optional_minor_and_patch=True)
|
27
|
+
except (TypeError, ValueError):
|
28
|
+
pass
|
29
|
+
|
30
|
+
if not default_version and not sem_version:
|
31
|
+
raise ValueError(f"{version_str} is not valid swupdate version")
|
32
|
+
|
33
|
+
return Version(version_str, default_version, sem_version)
|
34
|
+
|
35
|
+
def __str__(self):
|
36
|
+
return self.version_str
|
37
|
+
|
38
|
+
def __eq__(self, other):
|
39
|
+
# support comparison with strings as a convenience
|
40
|
+
if isinstance(other, str):
|
41
|
+
try:
|
42
|
+
other = Version.parse(other)
|
43
|
+
except ValueError:
|
44
|
+
return False
|
45
|
+
|
46
|
+
if not isinstance(other, Version):
|
47
|
+
return NotImplemented
|
48
|
+
|
49
|
+
if self.default_version and other.default_version:
|
50
|
+
return self.default_version == other.default_version
|
51
|
+
|
52
|
+
if self.sem_version and other.sem_version:
|
53
|
+
return self.sem_version == other.sem_version
|
54
|
+
|
55
|
+
# fallback to lexical comparison of no of the same type
|
56
|
+
return self.version_str == other.version_str
|
57
|
+
|
58
|
+
def __lt__(self, other):
|
59
|
+
if not isinstance(other, Version):
|
60
|
+
return NotImplemented
|
61
|
+
|
62
|
+
if self.default_version and other.default_version:
|
63
|
+
return self.default_version < other.default_version
|
64
|
+
|
65
|
+
if self.sem_version and other.sem_version:
|
66
|
+
return self.sem_version < other.sem_version
|
67
|
+
|
68
|
+
# fallback to lexical comparison of no of the same type
|
69
|
+
return self.version_str < other.version_str
|
70
|
+
|
71
|
+
@staticmethod
|
72
|
+
def _default_version_to_number(version_string: str) -> int | None:
|
73
|
+
parts = version_string.split(".")
|
74
|
+
count = min(len(parts), 4)
|
75
|
+
version = 0
|
76
|
+
|
77
|
+
for i in range(count):
|
78
|
+
try:
|
79
|
+
fld = int(parts[i])
|
80
|
+
except ValueError:
|
81
|
+
return None
|
82
|
+
|
83
|
+
if fld > 0xFFFF:
|
84
|
+
print(f"Version {version_string} had an element > 65535, falling back to semver")
|
85
|
+
return None
|
86
|
+
|
87
|
+
version = (version << 16) | fld
|
88
|
+
|
89
|
+
if count < 4:
|
90
|
+
version <<= 16 * (4 - count)
|
91
|
+
|
92
|
+
return version
|