Skip to content

Commit 93cce06

Browse files
mat-lgrSalim-Sycon
andauthored
feat: implement sycon python lib
* SYC-95 : create public python lib and associates tests/workflow * SYC-95 : fix workflow file * SYC-95 : fix workflow file * SYC-95 : fix README error * SYC-95 : chare Exceptions class in libs * SYC-95 : add Exceptions in init * SYC-95: Updating link to swagger in README.md * SYC-95 : fix RPP --------- Co-authored-by: Salim-Sycon <salim.khallouki@sycon.fr>
1 parent 3b6353c commit 93cce06

File tree

10 files changed

+1085
-1
lines changed

10 files changed

+1085
-1
lines changed

.github/workflows/ci_api_lib.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: CI — Run tests on API python lib
2+
3+
on:
4+
pull_request: {}
5+
workflow_dispatch: {}
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
timeout-minutes: 60
11+
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v4
15+
with:
16+
fetch-depth: 0
17+
18+
- name: Set up Python (ensure python3 is available)
19+
uses: actions/setup-python@v4
20+
with:
21+
python-version: '3.10'
22+
23+
- name: Show Python info
24+
run: |
25+
python --version
26+
which python
27+
pip --version
28+
29+
- name: Make run_tests.sh executable
30+
run: |
31+
cd libs/SyconApi
32+
chmod +x ./run_tests.sh
33+
34+
- name: Run tests script
35+
env:
36+
CI: "true"
37+
run: |
38+
cd libs/SyconApi
39+
./run_tests.sh
40+
41+
- name: Upload coverage HTML
42+
if: always()
43+
uses: actions/upload-artifact@v4
44+
with:
45+
name: coverage-html
46+
path: libs/SyconApi/htmlcov

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# auto generated
2+
.venv
3+
.pycache
4+
.pytest_cache
5+
htmlcov
6+
.coverage
7+
__pycache__
8+
SyconApi.egg-info

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ L’API utilise un **jeton JWT** transmis dans l’en-tête `Authorization` et u
4343

4444
## 3) Utiliser Swagger UI (pas à pas)
4545

46-
1. Ouvrez : <https://hello-sycon.github.io/sycon-api-production/>.
46+
1. Ouvrez : <https://hello-sycon.github.io/sycon-api/>.
4747
2. Cliquez **Authorize** et collez votre valeur `Bearer <JWT>` dans le champ prévu.
4848
3. Utilisez la **recherche** pour trouver un endpoint par nom ou par tag (« API auth », « Data API »).
4949
4. Dépliez un endpoint, cliquez **Try it out**, renseignez les paramètres, puis **Execute**.

libs/SyconApi/README.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# SyconApi
2+
3+
Lightweight Python client for the Sycon cloud API — concise, synchronous wrapper to authenticate and fetch device lists and raw device data.
4+
5+
## Install
6+
7+
```bash
8+
# HTTPS
9+
pip install "git+https://github.com/Hello-sycon/sycon-api.git@SyconApi-v$VERSION#subdirectory=libs/SyconApi"
10+
11+
#SSH
12+
pip install "git+ssh://git@github.com/Hello-sycon/sycon-api.git@SyconApi-v$VERSION#subdirectory=libs/SyconApi"
13+
```
14+
15+
>[!NOTE]
16+
>To select the $VERSION variable, you can see the release associate to tag section in github repository
17+
18+
## Quick example
19+
20+
```py
21+
from sycon_api import SyconApi
22+
23+
client = SyconApi(username="me@example.com", password="secret", debug=False)
24+
25+
# authenticate (normally called automatically by methods that need a token)
26+
client.authenticate()
27+
28+
# list devices
29+
devices = client.get_devices_list()
30+
print(devices)
31+
32+
# get data for a single device
33+
data = client.get_data_from_device(
34+
device_id="device-123",
35+
field=SyconApi.SyconApiDataFields.TEMPERATURE_CELSIUS,
36+
start_date="2025-10-03T12:30:00.000Z",
37+
end_date="2025-10-03T13:30:00.000Z",
38+
head_limit=100,
39+
)
40+
print(data)
41+
42+
# get data for multiple devices (use tuple — function is cached)
43+
devices_data = client.get_data_from_devices(
44+
devices_id=("device-123", "device-456"),
45+
field=SyconApi.SyconApiDataFields.CO2_PPM,
46+
start_date="2025-10-03T12:30:00.000Z",
47+
end_date="2025-10-03T13:30:00.000Z",
48+
head_limit=10,
49+
)
50+
print(devices_data)
51+
52+
# get data for all devices (uses get_devices_list internally)
53+
all_data = client.get_data_from_all_devices(
54+
field=SyconApi.SyconApiDataFields.HUMIDITY_PERCENT,
55+
start_date="2025-10-03T12:30:00.000Z",
56+
end_date="2025-10-03T13:30:00.000Z",
57+
head_limit=10,
58+
)
59+
print(all_data)
60+
```
61+
62+
## Public API (concise)
63+
64+
All methods below belong to `SyconApi` (no leading underscore).
65+
66+
### Constructor / properties
67+
68+
* `SyconApi(username: str, password: str, debug: bool = False, debug_level: int = SyconApiLogLevel.DEBUG.value) -> SyconApi`
69+
Create client. Pass `debug=True` to enable logger.
70+
* `username` — property, configured username.
71+
* `token` — property, current token (may be `None` until authenticated).
72+
73+
### Enums
74+
75+
* `SyconApi.SyconApiDataFields` — data field constants (e.g. `TEMPERATURE_CELSIUS`, `CO2_PPM`, `HUMIDITY_PERCENT`, ...).
76+
* `SyconApi.SyconApiV1Route` — endpoints (LOGIN, RENEW, CHECK, DEVICES, DATA).
77+
* `SyconApi.SyconApiLogLevel` — log levels.
78+
79+
### Exceptions you may catch
80+
81+
* `SyconApiInvalidParametersException` — invalid argument(s).
82+
* `SyconApiMissingParametersException` — required parameter missing.
83+
* `SyconApiBadResponseException` — 4xx HTTP response or invalid JSON where JSON is expected.
84+
* `SyconApiServerErrorResponseException` — 5xx HTTP response.
85+
86+
### Main methods
87+
88+
* `authenticate() -> None`
89+
Authenticate with username/password. On success the client `token` is set from response headers.
90+
91+
* `renew_token() -> None`
92+
Renew token using current token. Raises `SyconApiBadResponseException` if no `Authorization` header in response.
93+
94+
* `check_token() -> bool`
95+
Check if current token is still valid. Returns `True` on HTTP 200.
96+
97+
* `get_devices_list() -> Optional[Dict[str, Any]]`
98+
Return devices list (parsed JSON) on HTTP 200. Raises `SyconApiBadResponseException` if response JSON is invalid.
99+
100+
* `get_data_from_device(device_id: str, field: SyconApiDataFields, start_date: str, end_date: str, head_limit: Optional[int] = None, tail_limit: Optional[int] = None, external_sensor_id: Optional[str] = None) -> Optional[Dict[str, Any]]`
101+
Fetch raw data for a single device. Returns a `dict` if response JSON parsed, otherwise `None`. Validates date format (ISO-8601 instant) and requires exactly one of `head_limit` / `tail_limit`.
102+
103+
* `get_data_from_devices(devices_id: Tuple[str], field: SyconApiDataFields, start_date: str, end_date: str, head_limit: Optional[int] = None, tail_limit: Optional[int] = None, external_sensor_id: Optional[str] = None) -> Optional[Dict[str, Any]]`
104+
Fetch raw data for multiple devices. **Important:** `devices_id` must be an **immutable tuple** (function is cached via `lru_cache`). Returns a dict mapping `device_id` → response body (dict or raw text).
105+
106+
* `get_data_from_all_devices(field: SyconApiDataFields, start_date: str, end_date: str, head_limit: Optional[int] = None, tail_limit: Optional[int] = None, external_sensor_id: Optional[str] = None) -> Optional[Dict[str, Any]]`
107+
Fetch data for all devices returned by `get_devices_list()`. Skips device entries that do not have an `"id"` key.
108+
109+
## Notes & best practices
110+
111+
* **Dates:** use strict ISO-8601 instant format: `YYYY-MM-DDTHH:MM:SS[.ms]Z` (example: `2025-10-03T12:30:00.000Z`). Invalid format raises `SyconApiInvalidParametersException`.
112+
* **Head/Tail limits:** exactly one of `head_limit` or `tail_limit` must be provided (not both). Limits are capped to `k_size_batch_limit` (default 10000).
113+
* **Caching:** `get_data_from_devices` and `get_data_from_device` are cached (`lru_cache`). For cached calls, ensure all arguments are hashable (use `tuple` for device lists). You can clear the cache with e.g. `SyconApi.get_data_from_devices.cache_clear()`.
114+
* **Errors:** 4xx responses raise `SyconApiBadResponseException`; 5xx raise `SyconApiServerErrorResponseException`. Network-level errors (requests exceptions) will propagate.
115+
* **Logging:** enable `debug=True` in the constructor to activate the internal logger.

libs/SyconApi/pyproject.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[build-system]
2+
requires = ["setuptools>=69.5.1", "wheel>=0.43"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "SyconApi"
7+
version = "0.1.0"
8+
description = "Sycon python api to users"
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
license = { text = "MIT" }
12+
authors = [ { name = "Sycon", email = "dev@sycon.fr" } ]
13+
dependencies = [
14+
"requests>=2.32,<3.0",
15+
"tenacity>=9.0.0,<10.0.0",
16+
]
17+
18+
[tool.setuptools]
19+
package-dir = {"" = "src"}
20+
packages = ["sycon_api"]

libs/SyconApi/pytest.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# pytest.ini
2+
[pytest]
3+
minversion = 7.0
4+
addopts = -ra
5+
testpaths = tests

libs/SyconApi/run_tests.sh

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Usage: ./run_tests.sh [pytest args...]
5+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
cd "$REPO_ROOT"
7+
8+
if [ ! -f pyproject.toml ]; then
9+
echo "ERROR: pyproject.toml not found. Lance le script depuis la racine du repo." >&2
10+
exit 1
11+
fi
12+
13+
VENV_DIR=".venv"
14+
PYTHON_CMD="${PYTHON:-python3}"
15+
16+
echo ">>> Using Python: $($PYTHON_CMD --version 2>&1 | tr -d '\n')"
17+
# create venv if missing
18+
if [ ! -d "$VENV_DIR" ]; then
19+
echo ">>> Creating virtualenv in $VENV_DIR ..."
20+
$PYTHON_CMD -m venv "$VENV_DIR"
21+
fi
22+
23+
# activate venv
24+
# shellcheck source=/dev/null
25+
. "$VENV_DIR/bin/activate"
26+
27+
echo ">>> Upgrading pip / setuptools / wheel ..."
28+
pip install -U pip setuptools wheel >/dev/null
29+
30+
echo ">>> Installing package editable (pip install -e .) ..."
31+
if pip install -e . >/dev/null 2>&1; then
32+
echo ">>> Package installed editable."
33+
else
34+
echo ">>> Warning: pip install -e . failed — falling back to PYTHONPATH=src"
35+
export PYTHONPATH="$REPO_ROOT/src:${PYTHONPATH:-}"
36+
fi
37+
38+
echo ">>> Installing test deps (pytest, pytest-cov, requests-mock) ..."
39+
pip install -U pytest pytest-cov requests-mock >/dev/null
40+
41+
# run pytest with coverage; forward any user args
42+
PYTEST_ARGS=( --cov=sycon_api --cov-report=term-missing --cov-report=html -q )
43+
if [ "$#" -ne 0 ]; then
44+
PYTEST_ARGS+=( "$@" )
45+
fi
46+
47+
echo ">>> Running pytest ..."
48+
pytest "${PYTEST_ARGS[@]}"
49+
EXIT_CODE=$?
50+
51+
echo
52+
echo ">>> Done. Coverage HTML report: $REPO_ROOT/htmlcov/index.html"
53+
echo ">>> Return code: $EXIT_CODE"
54+
exit $EXIT_CODE
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from .sycon_api import ( SyconApi,
2+
SyconApiBadResponseException,
3+
SyconApiInvalidParametersException,
4+
SyconApiMissingParametersException,
5+
SyconApiServerErrorResponseException)
6+
__all__ = ["SyconApi",
7+
"SyconApiInvalidParametersException",
8+
"SyconApiMissingParametersException",
9+
"SyconApiBadResponseException",
10+
"SyconApiServerErrorResponseException"]

0 commit comments

Comments
 (0)