espark-core 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of espark-core might be problematic. Click here for more details.

@@ -1,29 +1,30 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: espark-core
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: The core module of the Espark ESP32-based IoT device management framework.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: aiomqtt==2.4.0
10
- Requires-Dist: apscheduler==3.11.1
11
- Requires-Dist: fastapi==0.124.0
12
- Requires-Dist: sqlalchemy==2.0.45
13
- Requires-Dist: sqlmodel==0.0.27
14
- Requires-Dist: uvicorn[standard]==0.38.0
9
+ Requires-Dist: aiomqtt
10
+ Requires-Dist: apscheduler
11
+ Requires-Dist: fastapi
12
+ Requires-Dist: packaging
13
+ Requires-Dist: sqlalchemy
14
+ Requires-Dist: sqlmodel
15
+ Requires-Dist: uvicorn[standard]
15
16
  Provides-Extra: dev
16
- Requires-Dist: aiosqlite==0.21.0; extra == "dev"
17
- Requires-Dist: autopep8==2.3.2; extra == "dev"
18
- Requires-Dist: build==1.3.0; extra == "dev"
19
- Requires-Dist: fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.16; extra == "dev"
20
- Requires-Dist: httpx==0.28.1; extra == "dev"
21
- Requires-Dist: pycodestyle==2.14.0; extra == "dev"
22
- Requires-Dist: pylint==4.0.4; extra == "dev"
23
- Requires-Dist: pytest==9.0.2; extra == "dev"
24
- Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
25
- Requires-Dist: pytest-cov==7.0.0; extra == "dev"
26
- Requires-Dist: twine==6.2.0; extra == "dev"
17
+ Requires-Dist: aiosqlite; extra == "dev"
18
+ Requires-Dist: autopep8; extra == "dev"
19
+ Requires-Dist: build; extra == "dev"
20
+ Requires-Dist: fastapi-cli[standard-no-fastapi-cloud-cli]; extra == "dev"
21
+ Requires-Dist: httpx; extra == "dev"
22
+ Requires-Dist: pycodestyle; extra == "dev"
23
+ Requires-Dist: pylint; extra == "dev"
24
+ Requires-Dist: pytest; extra == "dev"
25
+ Requires-Dist: pytest-asyncio; extra == "dev"
26
+ Requires-Dist: pytest-cov; extra == "dev"
27
+ Requires-Dist: twine; extra == "dev"
27
28
  Dynamic: description
28
29
  Dynamic: description-content-type
29
30
  Dynamic: license
@@ -1,29 +1,32 @@
1
- espark_core-0.3.0.dist-info/licenses/LICENSE,sha256=wkIXJHYIspOGVvn_ajKyLucpIyw9_pO3QExFcNTCY78,1065
1
+ espark_core-0.4.0.dist-info/licenses/LICENSE,sha256=wkIXJHYIspOGVvn_ajKyLucpIyw9_pO3QExFcNTCY78,1065
2
2
  esparkcore/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- esparkcore/constants.py,sha256=jgbhkana_NU_AdcGI0B0OjEOSBAuYNmTroC-vdlD69I,338
3
+ esparkcore/constants.py,sha256=uI0bVa78CZa-ftlyTRMqWSBK275iDB1l8g1TmtJ1FGg,378
4
4
  esparkcore/data/__init__.py,sha256=abMOPq6jC5qOQCl9j7iPDv8iyjzDF-oVd_gW0la5kL4,45
5
5
  esparkcore/data/database.py,sha256=OaJSBDW6Qge-rsKkSIksJXOg3xF_y6LuBew--6uFbG4,532
6
- esparkcore/data/models/__init__.py,sha256=y4tnMxZGRsqYTWw-ctWzPMHj1B9xfPYc3Ettoz2vYAI,92
7
- esparkcore/data/models/device.py,sha256=xXewJHBx4_R_m-N81m0_mcgNmbxzcK0KyCvIGazU3Ck,823
6
+ esparkcore/data/models/__init__.py,sha256=9YBRwn7R_aovQjT-ZR64fBm7TPs0dsjoq3LzsUTaRtA,124
7
+ esparkcore/data/models/device.py,sha256=HQr941uabovQpyGZ1fOnsBNSU_LVmB7JQIZuDkk1uis,1086
8
8
  esparkcore/data/models/outbox.py,sha256=r5bgjIbj9H-OsGhE-CWKGRf4Tp88qMvmxPYHNGOApY4,1309
9
9
  esparkcore/data/models/telemetry.py,sha256=OfzGqGj2Y1sdK519sSOEXhfuErHoeEdpy5qmJMIc27A,652
10
- esparkcore/data/repositories/__init__.py,sha256=uuzaSYEtrifTHYMXr7xGPwXLhvuvccGYfur6w9UG72E,195
10
+ esparkcore/data/models/version.py,sha256=h2O_Owx-exx5tiQiiauCSELkUHDx-QZjiIGqFPybT4M,269
11
+ esparkcore/data/repositories/__init__.py,sha256=R4cFUr137IxhyQVwFaXRHikPkTBE_4nUAAFRchxcohA,248
11
12
  esparkcore/data/repositories/base_repository.py,sha256=L5APTp8SczndbWqgJyEqKe-4NQoL_YYOapUN6i572cM,2097
12
13
  esparkcore/data/repositories/device_repository.py,sha256=P9KK-Ta9MgMdrnct5hbGEoipPOPrrDQ4wINQnZZ6iZY,1131
13
14
  esparkcore/data/repositories/outbox_repository.py,sha256=PdPyKq2Km4_7732O6HhA4BlV_zBjYF91GkMltNKUovg,1015
14
15
  esparkcore/data/repositories/telemetry_repository.py,sha256=5UBBmY-ugcgP0Xji7cu58_nGt8MWKeHz9DHNVJxk0BY,1116
15
- esparkcore/routers/__init__.py,sha256=kVr51MhvJYVKYZXS9maqf4XXj-4qLyM9UFk5x9BEAnQ,86
16
+ esparkcore/data/repositories/version_repository.py,sha256=QZZX2BpFx1i0oeFJ6j0nZvlZH7cViKU8VERBE8AU3ZY,395
17
+ esparkcore/routers/__init__.py,sha256=K8-S0hugkQT5DB8i8O5YqeRsu9fGfLep6UQzCIpdgGg,131
16
18
  esparkcore/routers/base_router.py,sha256=t-x8tYZVzBk9bD__9yaGYhm66glq_oM_lAHn0eplM2M,3902
17
19
  esparkcore/routers/device_router.py,sha256=mIBAyTyxl9AF8Os_Sms6Bb0vEvWfljyElbTWEGLo0iU,1172
18
- esparkcore/routers/telemetry_router.py,sha256=3ZpgY1fjGkXLmoaxm2paNJNVc5VMf1iZqsDh0xa0TyI,2014
20
+ esparkcore/routers/telemetry_router.py,sha256=sLO5-oN73IjdGC_ASNif5xmVZHjka_iLv0BKZ_deZP0,1981
21
+ esparkcore/routers/version_router.py,sha256=zoOteeK4Cs-OIk_k2v7vPwpR-OfLBwYhOUhNZjPYfHs,387
19
22
  esparkcore/schedules/__init__.py,sha256=v5Wy3Cbt2AnudGMGXv_Cyaa10s6NAmP66rDjcMSvf0o,74
20
23
  esparkcore/schedules/outbox.py,sha256=GGANvjkI7w1BdmmVRgKog498ASD_xRN0g8bG6yC_SN4,811
21
24
  esparkcore/schedules/scheduler.py,sha256=aYQXImLZ4tW-hRavn3ObuV3lYjmOZD813YkeG1NsYfY,170
22
25
  esparkcore/services/__init__.py,sha256=RIZOHBfkWn3HsApdA5rH9EZbbXGq0NfiGoOWDO_JX1k,30
23
- esparkcore/services/mqtt.py,sha256=292ZJ9YZbAYGghNFtRoXWbMenOiOo57bIo-D9Pwyr8o,4831
26
+ esparkcore/services/mqtt.py,sha256=9MOlFIKtR42CuOfX-kEd-DT2od7mXtbD-0B8dNHqplg,6279
24
27
  esparkcore/utils/__init__.py,sha256=rzuCJUAaQl-yPhM1vcWB_1zVXqtA71ChYP2aM_3UfKk,42
25
28
  esparkcore/utils/logging.py,sha256=sRO8uOcPkFH-DwepqLOTS2qGaopXpMdVOjzZL6RpGs4,257
26
- espark_core-0.3.0.dist-info/METADATA,sha256=X62hQPXtpsKFEIjJAgBXsFtNYd4NhuntK4kK9tyFlQA,6265
27
- espark_core-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- espark_core-0.3.0.dist-info/top_level.txt,sha256=hXVyhIPB4aGskFm5queWALxDPhcDkrN7_8vcjwA1iE4,11
29
- espark_core-0.3.0.dist-info/RECORD,,
29
+ espark_core-0.4.0.dist-info/METADATA,sha256=Z2PvZcnFDZncpr1gk2A-Yabnx-4kI5zCuS3UgjhubGg,6161
30
+ espark_core-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ espark_core-0.4.0.dist-info/top_level.txt,sha256=hXVyhIPB4aGskFm5queWALxDPhcDkrN7_8vcjwA1iE4,11
32
+ espark_core-0.4.0.dist-info/RECORD,,
esparkcore/constants.py CHANGED
@@ -2,8 +2,9 @@ ENV_DATABASE_URL : str = 'DATABASE_URL'
2
2
  ENV_MQTT_HOST : str = 'MQTT_HOST'
3
3
  ENV_MQTT_PORT : str = 'MQTT_PORT'
4
4
 
5
+ TOPIC_ACTION : str = 'espark/action'
5
6
  TOPIC_DEVICE : str = 'espark/device'
6
7
  TOPIC_REGISTRATION : str = 'espark/registration'
7
8
  TOPIC_TELEMETRY : str = 'espark/telemetry'
8
- TOPIC_ACTION : str = 'espark/action'
9
+ TOPIC_OTP : str = 'espark/otp'
9
10
  TOPIC_CRASH : str = 'espark/crash'
@@ -1,3 +1,4 @@
1
1
  from .device import Device
2
2
  from .outbox import OutboxEvent
3
3
  from .telemetry import Telemetry
4
+ from .version import AppVersion
@@ -8,6 +8,8 @@ from sqlmodel import SQLModel, Field, JSON
8
8
  class Device(SQLModel, table=True):
9
9
  id : str = Field(primary_key=True, description='Unique identifier for the device')
10
10
  display_name : Optional[str] = Field(default=None, description='Human-readable name of the device')
11
+ app_name : Optional[str] = Field(default=None, description='Name of the application running on the device')
12
+ app_version : Optional[str] = Field(default=None, description='Version of the application running on the device')
11
13
  capabilities : Optional[str] = Field(default=None, description='Comma separated capabilities of the device')
12
14
  parameters : Dict[str, str | int | bool] = Field(default_factory=dict, sa_column=Column(JSON), description='JSON string of capability-specific parameters')
13
15
  last_seen : datetime = Field(index=True, description='Last time the device was seen online')
@@ -0,0 +1,6 @@
1
+ from sqlmodel import SQLModel, Field
2
+
3
+
4
+ class AppVersion(SQLModel, table=True):
5
+ id : str = Field(primary_key=True, description='The app name, which is a unique identifier for the app version')
6
+ version : str = Field(description='Version of the application')
@@ -2,3 +2,4 @@ from .base_repository import AsyncRepository
2
2
  from .device_repository import DeviceRepository
3
3
  from .outbox_repository import OutboxRepository
4
4
  from .telemetry_repository import TelemetryRepository
5
+ from .version_repository import AppVersionRepository
@@ -0,0 +1,11 @@
1
+ from ..models import AppVersion
2
+ from .base_repository import AsyncRepository
3
+
4
+
5
+ class AppVersionRepository(AsyncRepository[AppVersion]):
6
+ def __init__(self):
7
+ super().__init__(AppVersion)
8
+
9
+ async def get_by_app_name(self, session, app_name: str) -> AppVersion | None:
10
+ # pylint: disable=unexpected-keyword-arg
11
+ return await self.get(session, AppVersion.id == app_name)
@@ -1,2 +1,3 @@
1
1
  from .device_router import DeviceRouter
2
2
  from .telemetry_router import TelemetryRouter
3
+ from .version_router import AppVersionRouter
@@ -1,5 +1,5 @@
1
1
  from datetime import datetime, timedelta, timezone
2
- from typing import cast, Sequence
2
+ from typing import Sequence
3
3
 
4
4
  from fastapi import Depends, Query, Response
5
5
  from sqlalchemy.ext.asyncio import AsyncSession
@@ -28,7 +28,7 @@ class TelemetryRouter(BaseRouter):
28
28
  capabilities = device.capabilities.split(',') if device.capabilities else []
29
29
  for data_type in capabilities:
30
30
  if not data_type.startswith('action_'):
31
- telemetry = await cast(TelemetryRepository, self.repo).get_latest_for_device(session, device.id, data_type)
31
+ telemetry = await self.repo.get_latest_for_device(session, device.id, data_type)
32
32
  if telemetry:
33
33
  if telemetry.timestamp.tzinfo is None:
34
34
  telemetry.timestamp = telemetry.timestamp.replace(tzinfo=timezone.utc)
@@ -0,0 +1,10 @@
1
+ from ..data.models import AppVersion
2
+ from ..data.repositories import AppVersionRepository
3
+ from .base_router import BaseRouter
4
+
5
+
6
+ class AppVersionRouter(BaseRouter):
7
+ def __init__(self, repo: AppVersionRepository = None) -> None:
8
+ self.repo : AppVersionRepository = repo or AppVersionRepository()
9
+
10
+ super().__init__(AppVersion, self.repo, '/api/v1/versions', ['version'])
@@ -1,27 +1,29 @@
1
1
  from asyncio import create_task, gather, Queue, sleep
2
2
  from datetime import datetime, timezone
3
- from json import JSONDecodeError, loads
3
+ from json import dumps, JSONDecodeError, loads
4
4
  from os import getenv
5
5
  from uuid import getnode
6
6
 
7
7
  from aiomqtt import Client, MqttError
8
+ from packaging.version import Version
8
9
 
9
- from ..constants import ENV_MQTT_HOST, ENV_MQTT_PORT, TOPIC_CRASH, TOPIC_REGISTRATION, TOPIC_TELEMETRY
10
+ from ..constants import ENV_MQTT_HOST, ENV_MQTT_PORT, TOPIC_CRASH, TOPIC_OTP, TOPIC_REGISTRATION, TOPIC_TELEMETRY
10
11
  from ..data import async_session
11
- from ..data.models import Device, Telemetry
12
- from ..data.repositories import DeviceRepository, TelemetryRepository
12
+ from ..data.models import AppVersion, Device, Telemetry
13
+ from ..data.repositories import AppVersionRepository, DeviceRepository, TelemetryRepository
13
14
  from ..utils import log_debug, log_error
14
15
 
15
16
  MQTT_RETRY_DELAY: int = 5
16
17
 
17
18
 
18
19
  class MQTTManager:
19
- def __init__(self, device_repo: DeviceRepository = None, telemetry_repo: TelemetryRepository = None):
20
- self.mqtt_host : str = getenv(ENV_MQTT_HOST, 'localhost')
21
- self.mqtt_port : int = int(getenv(ENV_MQTT_PORT, '1883'))
22
- self.device_repo : DeviceRepository = device_repo
23
- self.telemetry_repo : TelemetryRepository = telemetry_repo
24
- self.queue : Queue = Queue()
20
+ def __init__(self, version_repo: AppVersionRepository = None, device_repo: DeviceRepository = None, telemetry_repo: TelemetryRepository = None):
21
+ self.mqtt_host : str = getenv(ENV_MQTT_HOST, 'localhost')
22
+ self.mqtt_port : int = int(getenv(ENV_MQTT_PORT, '1883'))
23
+ self.version_repo : AppVersionRepository = version_repo if version_repo else AppVersionRepository()
24
+ self.device_repo : DeviceRepository = device_repo if device_repo else DeviceRepository()
25
+ self.telemetry_repo : TelemetryRepository = telemetry_repo if telemetry_repo else TelemetryRepository()
26
+ self.queue : Queue = Queue()
25
27
 
26
28
  async def _handle_registration(self, device_id: str, payload: dict) -> None:
27
29
  try:
@@ -30,6 +32,8 @@ class MQTTManager:
30
32
  async with async_session() as session:
31
33
  device = await self.device_repo.get(session, Device.id == device_id)
32
34
  if device:
35
+ device.app_name = payload['app_name']
36
+ device.app_version = payload['app_version']
33
37
  device.capabilities = payload['capabilities']
34
38
  device.last_seen = datetime.now(timezone.utc)
35
39
 
@@ -39,10 +43,26 @@ class MQTTManager:
39
43
 
40
44
  device.id = device_id
41
45
  device.display_name = None
46
+ device.app_name = payload['app_name']
47
+ device.app_version = payload['app_version']
42
48
  device.capabilities = payload['capabilities']
43
49
  device.last_seen = datetime.now(timezone.utc)
44
50
 
45
51
  await self.device_repo.add(session, device)
52
+
53
+ latest_version = await self.version_repo.get(session, AppVersion.id == payload['app_name'])
54
+ current_version = Version(payload['app_version'])
55
+
56
+ if Version(latest_version.version) > current_version:
57
+ log_debug(f'Device {device_id} is running an outdated version ({current_version} < {latest_version.version})')
58
+
59
+ async with Client(self.mqtt_host, self.mqtt_port, identifier=f'espark-core-{hex(getnode())}') as client:
60
+ await client.publish(f'{TOPIC_OTP}/{device_id}', dumps({
61
+ 'device_id' : device_id,
62
+ 'app_name' : payload['app_name'],
63
+ 'app_version' : latest_version.version,
64
+ 'download_url' : None,
65
+ }), qos=1)
46
66
  # pylint: disable=broad-exception-caught
47
67
  except Exception as e:
48
68
  log_error(e)