rcer-iot-client-pkg 0.1.0__tar.gz
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.
- rcer_iot_client_pkg-0.1.0/LICENSE +22 -0
- rcer_iot_client_pkg-0.1.0/PKG-INFO +82 -0
- rcer_iot_client_pkg-0.1.0/README.md +60 -0
- rcer_iot_client_pkg-0.1.0/pyproject.toml +44 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/__init__.py +8 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/general_types/__init__.py +0 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/general_types/api/__init__.py +0 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/general_types/api/update_thies_data_types.py +0 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/general_types/error_types/__init__.py +0 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/general_types/error_types/api/__init__.py +0 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/general_types/error_types/api/update_thies_data_error_types.py +19 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/general_types/error_types/common/__init__.py +7 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/general_types/error_types/common/common_types.py +13 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/async_http_client/__init__.py +10 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/async_http_client/async_http_client.py +34 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/async_http_client/async_http_client_contract.py +29 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/async_http_client/clients/__init__.py +0 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/async_http_client/clients/aiohttp_client.py +50 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/async_http_client/types/__init__.py +3 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/async_http_client/types/async_http_client_types.py +17 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/ftp_client/__init__.py +4 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/ftp_client/clients/__init__.py +0 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/ftp_client/clients/aioftp_client.py +34 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/ftp_client/ftp_client.py +22 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/ftp_client/ftp_client_contract.py +13 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/ftp_client/types/__init__.py +3 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/ftp_client/types/ftp_client_types.py +20 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/zero_dependency/utils/datetime_utils.py +25 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/__init__.py +0 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/api.py +23 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/controllers/__init__.py +5 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/controllers/types/__init__.py +6 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/controllers/types/update_thies_data_types.py +17 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/controllers/update_thies_data.py +68 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/use_cases/constants.py +4 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/use_cases/types/__init__.py +3 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/use_cases/types/update_thies_data_types.py +17 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/use_cases/update_thies_data.py +135 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/utils/__init__.py +3 -0
- rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/utils/update_thies_data_utils.py +18 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025, pedropablozavalat
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: rcer_iot_client_pkg
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A client library for IoT projects in the RCER initiative
|
5
|
+
License: MIT
|
6
|
+
Author: pedropablozavalat
|
7
|
+
Requires-Python: >=3.10,<4.0
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
14
|
+
Requires-Dist: aioftp (==0.25.1)
|
15
|
+
Requires-Dist: aiohttp (==3.11.16)
|
16
|
+
Requires-Dist: build
|
17
|
+
Requires-Dist: dotenv (==0.9.9)
|
18
|
+
Requires-Dist: pydantic (==2.11.3)
|
19
|
+
Requires-Dist: pytest-cov (==6.1.1)
|
20
|
+
Description-Content-Type: text/markdown
|
21
|
+
|
22
|
+
# rcer_iot_client_pkg
|
23
|
+
|
24
|
+
RCER IoT Client Library
|
25
|
+
|
26
|
+
## Installation
|
27
|
+
|
28
|
+
To install the package, run:
|
29
|
+
|
30
|
+
```bash
|
31
|
+
pip install rcer_iot_client_pkg
|
32
|
+
```
|
33
|
+
|
34
|
+
## Development
|
35
|
+
|
36
|
+
This project includes a `Makefile` to simplify common tasks. Below are the available commands:
|
37
|
+
|
38
|
+
### Install Basic Dependencies
|
39
|
+
To install the basic dependencies required for the project, run the following command:
|
40
|
+
|
41
|
+
```bash
|
42
|
+
make install-deps
|
43
|
+
```
|
44
|
+
|
45
|
+
This will ensure that all necessary libraries and tools are installed for the project to function properly.
|
46
|
+
|
47
|
+
### Install Development Requirements
|
48
|
+
For setting up a development environment with additional tools and libraries, execute:
|
49
|
+
|
50
|
+
```bash
|
51
|
+
make dev
|
52
|
+
```
|
53
|
+
|
54
|
+
This command installs all the dependencies needed for development, including testing and linting tools.
|
55
|
+
|
56
|
+
### Run Tests
|
57
|
+
To verify that the code is functioning as expected, you can run the test suite using:
|
58
|
+
|
59
|
+
```bash
|
60
|
+
make test
|
61
|
+
```
|
62
|
+
|
63
|
+
This will execute all the tests in the project and provide a summary of the results.
|
64
|
+
|
65
|
+
### Lint the Code
|
66
|
+
To ensure that the code adheres to the project's style guidelines and is free of common errors, run:
|
67
|
+
|
68
|
+
```bash
|
69
|
+
make lint
|
70
|
+
```
|
71
|
+
|
72
|
+
This command checks the codebase for linting issues and outputs any problems that need to be addressed.
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
If you're interested in contributing to this project, please follow the contributing guidelines. Contributions are welcome and appreciated!
|
76
|
+
|
77
|
+
Interested in contributing? Check out the contributing guidelines. Please note that this project is released with a Code of Conduct. By contributing to this project, you agree to abide by its terms.
|
78
|
+
|
79
|
+
## License
|
80
|
+
|
81
|
+
`rcer_iot_client_pkg` was created by @pedrozavalat. It is licensed under the terms of the MIT license.
|
82
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# rcer_iot_client_pkg
|
2
|
+
|
3
|
+
RCER IoT Client Library
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
To install the package, run:
|
8
|
+
|
9
|
+
```bash
|
10
|
+
pip install rcer_iot_client_pkg
|
11
|
+
```
|
12
|
+
|
13
|
+
## Development
|
14
|
+
|
15
|
+
This project includes a `Makefile` to simplify common tasks. Below are the available commands:
|
16
|
+
|
17
|
+
### Install Basic Dependencies
|
18
|
+
To install the basic dependencies required for the project, run the following command:
|
19
|
+
|
20
|
+
```bash
|
21
|
+
make install-deps
|
22
|
+
```
|
23
|
+
|
24
|
+
This will ensure that all necessary libraries and tools are installed for the project to function properly.
|
25
|
+
|
26
|
+
### Install Development Requirements
|
27
|
+
For setting up a development environment with additional tools and libraries, execute:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
make dev
|
31
|
+
```
|
32
|
+
|
33
|
+
This command installs all the dependencies needed for development, including testing and linting tools.
|
34
|
+
|
35
|
+
### Run Tests
|
36
|
+
To verify that the code is functioning as expected, you can run the test suite using:
|
37
|
+
|
38
|
+
```bash
|
39
|
+
make test
|
40
|
+
```
|
41
|
+
|
42
|
+
This will execute all the tests in the project and provide a summary of the results.
|
43
|
+
|
44
|
+
### Lint the Code
|
45
|
+
To ensure that the code adheres to the project's style guidelines and is free of common errors, run:
|
46
|
+
|
47
|
+
```bash
|
48
|
+
make lint
|
49
|
+
```
|
50
|
+
|
51
|
+
This command checks the codebase for linting issues and outputs any problems that need to be addressed.
|
52
|
+
|
53
|
+
## Contributing
|
54
|
+
If you're interested in contributing to this project, please follow the contributing guidelines. Contributions are welcome and appreciated!
|
55
|
+
|
56
|
+
Interested in contributing? Check out the contributing guidelines. Please note that this project is released with a Code of Conduct. By contributing to this project, you agree to abide by its terms.
|
57
|
+
|
58
|
+
## License
|
59
|
+
|
60
|
+
`rcer_iot_client_pkg` was created by @pedrozavalat. It is licensed under the terms of the MIT license.
|
@@ -0,0 +1,44 @@
|
|
1
|
+
[tool.poetry]
|
2
|
+
name = "rcer_iot_client_pkg"
|
3
|
+
version = "0.1.0"
|
4
|
+
description = "A client library for IoT projects in the RCER initiative"
|
5
|
+
authors = ["pedropablozavalat"]
|
6
|
+
license = "MIT"
|
7
|
+
readme = "README.md"
|
8
|
+
|
9
|
+
[tool.poetry.dependencies]
|
10
|
+
python = "^3.10"
|
11
|
+
aioftp = "0.25.1"
|
12
|
+
aiohttp = "3.11.16"
|
13
|
+
pydantic = "2.11.3"
|
14
|
+
dotenv = "0.9.9"
|
15
|
+
pytest-cov="6.1.1"
|
16
|
+
build="*"
|
17
|
+
[tool.poetry.group.dev.dependencies]
|
18
|
+
pytest = "8.3.5"
|
19
|
+
pytest-asyncio = "0.26.0"
|
20
|
+
black = "*"
|
21
|
+
coverage = "*"
|
22
|
+
flake8 = "*"
|
23
|
+
pyflakes = "*"
|
24
|
+
pylint = "*"
|
25
|
+
build = "*"
|
26
|
+
|
27
|
+
[tool.semantic_release]
|
28
|
+
version_toml = [
|
29
|
+
"pyproject.toml:tool.poetry.version",
|
30
|
+
] # version location
|
31
|
+
branch = "main" # branch to make releases of
|
32
|
+
changelog_file = "CHANGELOG.md" # changelog file
|
33
|
+
build_command = "pip install poetry && poetry build" # build dists
|
34
|
+
|
35
|
+
[build-system]
|
36
|
+
requires = ["poetry-core>=1.0.0"]
|
37
|
+
build-backend = "poetry.core.masonry.api"
|
38
|
+
|
39
|
+
[tool.ruff]
|
40
|
+
line-length = 88
|
41
|
+
target-version = "py311"
|
42
|
+
exclude = ["venv", ".venv", "build", "dist", "__pycache__"]
|
43
|
+
fix = true
|
44
|
+
lint.ignore = ["E501"]
|
File without changes
|
File without changes
|
rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/general_types/api/update_thies_data_types.py
ADDED
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class ThiesUploadEmptyError(Exception):
|
2
|
+
"""Raised when no files are found to upload to the server."""
|
3
|
+
|
4
|
+
def __str__(self):
|
5
|
+
return "No files were found to upload."
|
6
|
+
|
7
|
+
|
8
|
+
class FetchCloudFileNamesError(Exception):
|
9
|
+
"""Raised when there is an error fetching file names from the RCER cloud."""
|
10
|
+
|
11
|
+
def __str__(self):
|
12
|
+
return "An error occurred while retrieving file names from the RCER cloud"
|
13
|
+
|
14
|
+
|
15
|
+
class FetchThiesFileContentError(Exception):
|
16
|
+
"""Raised when there is an error fetching the content of a Thies file."""
|
17
|
+
|
18
|
+
def __str__(self):
|
19
|
+
return "An error occurred while retrieving the content of a Thies file"
|
rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/general_types/error_types/common/common_types.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class EmptyDataError(Exception):
|
2
|
+
def __str__(self):
|
3
|
+
return "The data provided is empty."
|
4
|
+
|
5
|
+
|
6
|
+
class HttpClientError(Exception):
|
7
|
+
def __str__(self):
|
8
|
+
return "Http Client initialization fails."
|
9
|
+
|
10
|
+
|
11
|
+
class FtpClientError(Exception):
|
12
|
+
def __str__(self):
|
13
|
+
return "Ftp Client initialization fails."
|
@@ -0,0 +1,10 @@
|
|
1
|
+
"""Export defined type classes."""
|
2
|
+
|
3
|
+
from .async_http_client import AsyncHTTPClient
|
4
|
+
from .types.async_http_client_types import (
|
5
|
+
AsyncHttpClientInitArgs,
|
6
|
+
GetArgs,
|
7
|
+
UploadFileArgs,
|
8
|
+
)
|
9
|
+
|
10
|
+
__all__ = ["AsyncHTTPClient", "AsyncHttpClientInitArgs", "GetArgs", "UploadFileArgs"]
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from .async_http_client_contract import AsyncHTTPClientContract
|
4
|
+
from .clients.aiohttp_client import AioHttpClient
|
5
|
+
from .types.async_http_client_types import (
|
6
|
+
AsyncHttpClientInitArgs,
|
7
|
+
GetArgs,
|
8
|
+
UploadFileArgs,
|
9
|
+
)
|
10
|
+
|
11
|
+
|
12
|
+
class AsyncHTTPClient(AsyncHTTPClientContract):
|
13
|
+
CLIENTS = {"aiohttp_client"}
|
14
|
+
|
15
|
+
def __init__(self, args: AsyncHttpClientInitArgs) -> None:
|
16
|
+
if args.client_name not in AsyncHTTPClient.CLIENTS:
|
17
|
+
msg = f"Unsupported client '{args.client_name}'"
|
18
|
+
raise KeyError(msg)
|
19
|
+
self.client_name = args.client_name
|
20
|
+
|
21
|
+
if args.client_name == "aiohttp_client":
|
22
|
+
self.client_obj = AioHttpClient(args)
|
23
|
+
|
24
|
+
async def __aenter__(self):
|
25
|
+
return await self.client_obj.__aenter__()
|
26
|
+
|
27
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
28
|
+
await self.client_obj.__aexit__(exc_type, exc_val, exc_tb)
|
29
|
+
|
30
|
+
async def get(self, args: GetArgs) -> dict[str, Any]:
|
31
|
+
return await self.client_obj.get(args)
|
32
|
+
|
33
|
+
async def upload_file(self, args: UploadFileArgs) -> dict[str, Any]:
|
34
|
+
return await self.client_obj.upload_file(args)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from .types.async_http_client_types import GetArgs, UploadFileArgs
|
5
|
+
|
6
|
+
|
7
|
+
class AsyncHTTPClientContract(ABC):
|
8
|
+
"""
|
9
|
+
A contract for asynchronous HTTP client implementations.
|
10
|
+
|
11
|
+
This abstract base class defines the required methods for performing
|
12
|
+
HTTP GET requests and uploading files asynchronously.
|
13
|
+
|
14
|
+
Methods.
|
15
|
+
-------
|
16
|
+
get(args: GetArgs) -> dict[str, Any]
|
17
|
+
Perform an HTTP GET request with the specified arguments.
|
18
|
+
|
19
|
+
upload_file(args: UploadFileArgs) -> dict[str, Any]
|
20
|
+
Upload a file using the specified arguments.
|
21
|
+
"""
|
22
|
+
|
23
|
+
@abstractmethod
|
24
|
+
async def get(self, args: GetArgs) -> dict[str, Any]:
|
25
|
+
pass
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
async def upload_file(self, args: UploadFileArgs) -> dict[str, Any]:
|
29
|
+
pass
|
File without changes
|
rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/async_http_client/clients/aiohttp_client.py
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from aiohttp import ClientError, ClientSession
|
4
|
+
|
5
|
+
from src.rcer_iot_client_pkg.libs.async_http_client.async_http_client_contract import (
|
6
|
+
AsyncHTTPClientContract,
|
7
|
+
)
|
8
|
+
from src.rcer_iot_client_pkg.libs.async_http_client.types.async_http_client_types import (
|
9
|
+
AsyncHttpClientInitArgs,
|
10
|
+
GetArgs,
|
11
|
+
UploadFileArgs,
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
class AioHttpClient(AsyncHTTPClientContract):
|
16
|
+
def __init__(self, args: AsyncHttpClientInitArgs) -> None:
|
17
|
+
self.access_token = args.access_token
|
18
|
+
self.base_url = args.base_url
|
19
|
+
self.headers = self._build_headers()
|
20
|
+
self.session: ClientSession | None = None
|
21
|
+
|
22
|
+
def _build_headers(self) -> dict:
|
23
|
+
return {"Authorization": f"Bearer {self.access_token}"}
|
24
|
+
|
25
|
+
async def __aenter__(self) -> "AioHttpClient":
|
26
|
+
self.session = ClientSession(headers=self.headers, base_url=self.base_url)
|
27
|
+
return self
|
28
|
+
|
29
|
+
async def __aexit__(
|
30
|
+
self, _exc_type: type[BaseException], _exc_val: BaseException, _exc_tb: Any
|
31
|
+
) -> None:
|
32
|
+
await self.session.close()
|
33
|
+
|
34
|
+
async def get(self, args: GetArgs) -> dict[str, Any]:
|
35
|
+
try:
|
36
|
+
endpoint, params = args.endpoint.lstrip("/"), args.params
|
37
|
+
response = await self.session.get(endpoint, params=params)
|
38
|
+
response.raise_for_status()
|
39
|
+
return await response.json()
|
40
|
+
except ClientError as error:
|
41
|
+
raise ConnectionError(error) from error
|
42
|
+
|
43
|
+
async def upload_file(self, args: UploadFileArgs) -> dict[str, Any]:
|
44
|
+
try:
|
45
|
+
endpoint, file_bytes = args.endpoint.lstrip("/"), args.file_bytes
|
46
|
+
response = await self.session.put(endpoint, data=file_bytes)
|
47
|
+
response.raise_for_status()
|
48
|
+
return await response.json()
|
49
|
+
except ClientError as error:
|
50
|
+
raise ConnectionError(error) from error
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from pydantic import BaseModel, Field
|
2
|
+
|
3
|
+
|
4
|
+
class AsyncHttpClientInitArgs(BaseModel):
|
5
|
+
access_token: str
|
6
|
+
base_url: str
|
7
|
+
client_name: str = "aiohttp_client"
|
8
|
+
|
9
|
+
|
10
|
+
class GetArgs(BaseModel):
|
11
|
+
endpoint: str
|
12
|
+
params: dict | None = Field(default=None)
|
13
|
+
|
14
|
+
|
15
|
+
class UploadFileArgs(BaseModel):
|
16
|
+
endpoint: str
|
17
|
+
file_bytes: bytes
|
File without changes
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from aioftp import Client
|
2
|
+
|
3
|
+
from src.rcer_iot_client_pkg.libs.ftp_client.ftp_client_contract import (
|
4
|
+
FTPClientContract,
|
5
|
+
)
|
6
|
+
from src.rcer_iot_client_pkg.libs.ftp_client.types.ftp_client_types import (
|
7
|
+
FtpClientInitArgs,
|
8
|
+
ListFilesArgs,
|
9
|
+
ReadFileArgs,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class AioFTPClient(FTPClientContract):
|
14
|
+
def __init__(self, args: FtpClientInitArgs) -> None:
|
15
|
+
self.host = args.host
|
16
|
+
self.port = args.port
|
17
|
+
self.password = args.password
|
18
|
+
self.user = args.user
|
19
|
+
self.client = Client()
|
20
|
+
|
21
|
+
async def _async_start(self) -> None:
|
22
|
+
await self.client.connect(host=self.host, port=self.port)
|
23
|
+
await self.client.login(user=self.user, password=self.password)
|
24
|
+
|
25
|
+
async def list_files(self, args: ListFilesArgs) -> list[str]:
|
26
|
+
await self._async_start()
|
27
|
+
return [
|
28
|
+
path.name async for path, _ in self.client.list(args.path, recursive=False)
|
29
|
+
]
|
30
|
+
|
31
|
+
async def read_file(self, args: ReadFileArgs) -> bytes:
|
32
|
+
await self._async_start()
|
33
|
+
async with self.client.download_stream(args.file_path) as stream:
|
34
|
+
return await stream.read()
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from .clients.aioftp_client import AioFTPClient
|
2
|
+
from .ftp_client_contract import FTPClientContract
|
3
|
+
from .types.ftp_client_types import FtpClientInitArgs, ListFilesArgs, ReadFileArgs
|
4
|
+
|
5
|
+
|
6
|
+
class FTPClient(FTPClientContract):
|
7
|
+
CLIENTS = {"aioftp_client"}
|
8
|
+
|
9
|
+
def __init__(self, args: FtpClientInitArgs) -> None:
|
10
|
+
if args.client_name not in FTPClient.CLIENTS:
|
11
|
+
msg = f"Unsupported client {args.client_name}"
|
12
|
+
raise KeyError(msg)
|
13
|
+
|
14
|
+
if args.client_name == "aioftp_client":
|
15
|
+
self.client_obj = AioFTPClient(args)
|
16
|
+
self.client_name = args.client_name
|
17
|
+
|
18
|
+
def list_files(self, args: ListFilesArgs) -> list[str]:
|
19
|
+
return self.client_obj.list_files(args)
|
20
|
+
|
21
|
+
def read_file(self, args: ReadFileArgs) -> bytes:
|
22
|
+
return self.client_obj.read_file(args)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
from .types.ftp_client_types import ListFilesArgs, ReadFileArgs
|
4
|
+
|
5
|
+
|
6
|
+
class FTPClientContract(ABC):
|
7
|
+
@abstractmethod
|
8
|
+
def list_files(self, args: ListFilesArgs) -> list[str]:
|
9
|
+
pass
|
10
|
+
|
11
|
+
@abstractmethod
|
12
|
+
def read_file(self, args: ReadFileArgs) -> bytes:
|
13
|
+
pass
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
|
3
|
+
|
4
|
+
@dataclass
|
5
|
+
class FtpClientInitArgs:
|
6
|
+
host: str
|
7
|
+
user: str
|
8
|
+
password: str
|
9
|
+
client_name: str = "aioftp_client"
|
10
|
+
port: int = 21
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class ListFilesArgs:
|
15
|
+
path: str
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class ReadFileArgs:
|
20
|
+
file_path: str
|
rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/libs/zero_dependency/utils/datetime_utils.py
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
from zoneinfo import ZoneInfo
|
3
|
+
|
4
|
+
|
5
|
+
def today(timezone: str = "America/Santiago") -> str:
|
6
|
+
"""
|
7
|
+
Return the current date.
|
8
|
+
|
9
|
+
:param timezone: A string representing the IANA timezone name.
|
10
|
+
Defaults to "America/Santiago".
|
11
|
+
:return datetime:
|
12
|
+
"""
|
13
|
+
return datetime.now(tz=ZoneInfo(timezone))
|
14
|
+
|
15
|
+
|
16
|
+
def datetime_to_str(date: datetime, date_format: str = "%Y-%m-%dT%H:%M%:S") -> str:
|
17
|
+
"""
|
18
|
+
Convert a datetime object to a string in the specified format.
|
19
|
+
|
20
|
+
:param date: The datetime object to convert.
|
21
|
+
:param date_format: The format to convert the datetime object to.
|
22
|
+
Defaults to "YYYYMMDD:HHMMSS".
|
23
|
+
:return: A string in the specified format.
|
24
|
+
"""
|
25
|
+
return date.strftime(date_format)
|
File without changes
|
@@ -0,0 +1,23 @@
|
|
1
|
+
from typing import Dict
|
2
|
+
|
3
|
+
from src.rcer_iot_client_pkg.services.epii.controllers import UpdateThiesDataController
|
4
|
+
from src.rcer_iot_client_pkg.services.epii.controllers.types import UpdateThiesDataControllerInput
|
5
|
+
|
6
|
+
class EpiiAPI:
|
7
|
+
def update_thies_data(
|
8
|
+
self,
|
9
|
+
ftp_port: int,
|
10
|
+
ftp_host: str,
|
11
|
+
ftp_password: str,
|
12
|
+
ftp_user: str,
|
13
|
+
) -> Dict[str, any]:
|
14
|
+
controller = UpdateThiesDataController(
|
15
|
+
UpdateThiesDataControllerInput(
|
16
|
+
ftp_port=ftp_port,
|
17
|
+
ftp_host=ftp_host,
|
18
|
+
ftp_password=ftp_password,
|
19
|
+
ftp_user=ftp_user,
|
20
|
+
)
|
21
|
+
)
|
22
|
+
response = controller.execute()
|
23
|
+
return response.__dict__
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from typing import Dict
|
3
|
+
|
4
|
+
|
5
|
+
@dataclass
|
6
|
+
class UpdateThiesDataControllerInput:
|
7
|
+
ftp_host: str
|
8
|
+
ftp_port: str
|
9
|
+
ftp_user: str
|
10
|
+
ftp_password: str
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class UpdateThiesDataControllerOutput:
|
15
|
+
message: str
|
16
|
+
status: int
|
17
|
+
metadata: Dict[str, str] = field(default_factory=dict)
|
rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/controllers/update_thies_data.py
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
from http import HTTPStatus
|
2
|
+
|
3
|
+
from src.rcer_iot_client_pkg.general_types.error_types.api.update_thies_data_error_types import (
|
4
|
+
FetchCloudFileNamesError,
|
5
|
+
ThiesUploadEmptyError,
|
6
|
+
)
|
7
|
+
from src.rcer_iot_client_pkg.general_types.error_types.common import (
|
8
|
+
FtpClientError,
|
9
|
+
HttpClientError,
|
10
|
+
)
|
11
|
+
from src.rcer_iot_client_pkg.services.epii.controllers.types.update_thies_data_types import (
|
12
|
+
UpdateThiesDataControllerInput,
|
13
|
+
UpdateThiesDataControllerOutput,
|
14
|
+
)
|
15
|
+
from src.rcer_iot_client_pkg.services.epii.use_cases.types import (
|
16
|
+
UpdateThiesDataUseCaseInput,
|
17
|
+
)
|
18
|
+
from src.rcer_iot_client_pkg.services.epii.use_cases.update_thies_data import (
|
19
|
+
UpdateThiesDataUseCase,
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
class UpdateThiesDataController:
|
24
|
+
def __init__(self, input: UpdateThiesDataControllerInput):
|
25
|
+
self.use_case = UpdateThiesDataUseCase(
|
26
|
+
UpdateThiesDataUseCaseInput(**input.__dict__)
|
27
|
+
)
|
28
|
+
|
29
|
+
async def execute(self) -> UpdateThiesDataControllerOutput:
|
30
|
+
try:
|
31
|
+
data = await self.use_case.execute()
|
32
|
+
return UpdateThiesDataControllerOutput(
|
33
|
+
message="THIES was synced successfully",
|
34
|
+
status=HTTPStatus.OK,
|
35
|
+
metadata={"data": data},
|
36
|
+
)
|
37
|
+
except (AttributeError, NameError, ValueError) as error:
|
38
|
+
return UpdateThiesDataControllerOutput(
|
39
|
+
message="An unexpected error occurred during use case initialization.",
|
40
|
+
status=HTTPStatus.BAD_REQUEST,
|
41
|
+
metadata={"error": error.__str__()},
|
42
|
+
)
|
43
|
+
except FtpClientError as error:
|
44
|
+
return UpdateThiesDataControllerOutput(
|
45
|
+
message="Ftp Client initialization fails.",
|
46
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
47
|
+
metadata={"error": error.__str__()},
|
48
|
+
)
|
49
|
+
|
50
|
+
except HttpClientError as error:
|
51
|
+
return UpdateThiesDataControllerOutput(
|
52
|
+
message="Http Client initialization fails.",
|
53
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
54
|
+
metadata={"error": error.__str__()},
|
55
|
+
)
|
56
|
+
|
57
|
+
except FetchCloudFileNamesError as error:
|
58
|
+
return UpdateThiesDataControllerOutput(
|
59
|
+
message="An error occurred while retrieving file names from the RCER cloud",
|
60
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
61
|
+
metadata={"error": error.__str__()},
|
62
|
+
)
|
63
|
+
except ThiesUploadEmptyError as error:
|
64
|
+
return UpdateThiesDataControllerOutput(
|
65
|
+
message="No files were found to upload.",
|
66
|
+
status=HTTPStatus.NO_CONTENT,
|
67
|
+
metadata={"error": error.__str__()},
|
68
|
+
)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from typing import Dict
|
3
|
+
|
4
|
+
|
5
|
+
@dataclass
|
6
|
+
class UpdateThiesDataUseCaseInput:
|
7
|
+
ftp_host: str
|
8
|
+
ftp_port: str
|
9
|
+
ftp_user: str
|
10
|
+
ftp_password: str
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class UpdateThiesDataUseCaseOutput:
|
15
|
+
message: str
|
16
|
+
status: int = 0
|
17
|
+
metadata: Dict[str, str] = field(default_factory=dict)
|
rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/use_cases/update_thies_data.py
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
from dotenv import load_dotenv
|
4
|
+
|
5
|
+
import src.rcer_iot_client_pkg.services.epii.use_cases.constants as c
|
6
|
+
from src.rcer_iot_client_pkg.general_types.error_types.api.update_thies_data_error_types import (
|
7
|
+
FetchCloudFileNamesError,
|
8
|
+
FetchThiesFileContentError,
|
9
|
+
ThiesUploadEmptyError,
|
10
|
+
)
|
11
|
+
from src.rcer_iot_client_pkg.general_types.error_types.common import (
|
12
|
+
EmptyDataError,
|
13
|
+
FtpClientError,
|
14
|
+
HttpClientError,
|
15
|
+
)
|
16
|
+
from src.rcer_iot_client_pkg.libs.async_http_client import (
|
17
|
+
AsyncHTTPClient,
|
18
|
+
AsyncHttpClientInitArgs,
|
19
|
+
GetArgs,
|
20
|
+
)
|
21
|
+
from src.rcer_iot_client_pkg.libs.ftp_client import (
|
22
|
+
FTPClient,
|
23
|
+
FtpClientInitArgs,
|
24
|
+
ListFilesArgs,
|
25
|
+
ReadFileArgs,
|
26
|
+
)
|
27
|
+
from src.rcer_iot_client_pkg.services.epii.use_cases.types import (
|
28
|
+
UpdateThiesDataUseCaseInput,
|
29
|
+
)
|
30
|
+
from src.rcer_iot_client_pkg.services.epii.utils import (
|
31
|
+
generate_file_content,
|
32
|
+
)
|
33
|
+
|
34
|
+
load_dotenv()
|
35
|
+
|
36
|
+
|
37
|
+
class UpdateThiesDataUseCase:
|
38
|
+
def __init__(self, input: UpdateThiesDataUseCaseInput):
|
39
|
+
self.ftp_port = input.ftp_port
|
40
|
+
self.ftp_host = input.ftp_host
|
41
|
+
self.ftp_password = input.ftp_password
|
42
|
+
self.ftp_user = input.ftp_user
|
43
|
+
self.sharepoint_client = self._initialize_sharepoint_client()
|
44
|
+
self.thies_ftp_client = self._initialize_thies_ftp_client()
|
45
|
+
self.uploading = set()
|
46
|
+
|
47
|
+
def _initialize_sharepoint_client(self) -> AsyncHTTPClient:
|
48
|
+
"""Initialize the HTTP client."""
|
49
|
+
try:
|
50
|
+
return AsyncHTTPClient(
|
51
|
+
AsyncHttpClientInitArgs(
|
52
|
+
client_name="aiohttp_client",
|
53
|
+
access_token="temporal-token",
|
54
|
+
base_url="https://graph.microsoft.com/v1.0/",
|
55
|
+
)
|
56
|
+
)
|
57
|
+
except Exception as error:
|
58
|
+
raise HttpClientError(error)
|
59
|
+
|
60
|
+
def _initialize_thies_ftp_client(self) -> FTPClient:
|
61
|
+
"""Initialize the FTP client."""
|
62
|
+
try:
|
63
|
+
return FTPClient(
|
64
|
+
FtpClientInitArgs(
|
65
|
+
client_name="aioftp_client",
|
66
|
+
host=self.ftp_host,
|
67
|
+
user=self.ftp_user,
|
68
|
+
password=self.ftp_password,
|
69
|
+
port=self.ftp_port,
|
70
|
+
)
|
71
|
+
)
|
72
|
+
except Exception as error:
|
73
|
+
raise FtpClientError(error)
|
74
|
+
|
75
|
+
async def fetch_cloud_file_names(self, folder_name: str) -> set[str]:
|
76
|
+
"""Fetch file names from the RCER cloud."""
|
77
|
+
try:
|
78
|
+
cloud_files = set()
|
79
|
+
async with self.sharepoint_client:
|
80
|
+
for file_type in c.FILE_TYPES:
|
81
|
+
destination_path = f"Onedrive_UC/noveno-semestre/IPRE-RCER/{folder_name}/{file_type}"
|
82
|
+
endpoint = f"drives/{c.DRIVE_ID}/root:/{destination_path}:/children"
|
83
|
+
response = await self.sharepoint_client.get(
|
84
|
+
GetArgs(endpoint=endpoint)
|
85
|
+
)
|
86
|
+
cloud_files.update(
|
87
|
+
{f"{file_type}_{item['name']}" for item in response["value"]}
|
88
|
+
)
|
89
|
+
return cloud_files
|
90
|
+
except ConnectionError as error:
|
91
|
+
raise FetchCloudFileNamesError(error)
|
92
|
+
|
93
|
+
async def fetch_thies_file_names(self) -> set[str]:
|
94
|
+
"""Fetch file names from the THIES FTP server."""
|
95
|
+
try:
|
96
|
+
avg_files = await self.thies_ftp_client.list_files(
|
97
|
+
ListFilesArgs(path=c.PATH_AVG_FILES)
|
98
|
+
)
|
99
|
+
ext_files = await self.thies_ftp_client.list_files(
|
100
|
+
ListFilesArgs(path=c.PATH_EXT_FILES)
|
101
|
+
)
|
102
|
+
return {f"AVG_{name}" for name in avg_files} | {
|
103
|
+
f"EXT_{name}" for name in ext_files
|
104
|
+
}
|
105
|
+
except ConnectionError:
|
106
|
+
raise ThiesUploadEmptyError
|
107
|
+
|
108
|
+
async def fetch_thies_file_content(self) -> dict[str, bytes]:
|
109
|
+
"""Fetch the content of files from the THIES FTP server."""
|
110
|
+
content_files = {}
|
111
|
+
for file in self.uploading:
|
112
|
+
try:
|
113
|
+
origin, filename = file.split("_", 1)
|
114
|
+
file_path = (
|
115
|
+
f"{c.PATH_AVG_FILES}/{filename}"
|
116
|
+
if origin == "AVG"
|
117
|
+
else f"{c.PATH_EXT_FILES}/{filename}"
|
118
|
+
)
|
119
|
+
content = await self.thies_ftp_client.read_file(ReadFileArgs(file_path))
|
120
|
+
content_files[filename] = content
|
121
|
+
except ConnectionError as error:
|
122
|
+
raise FetchThiesFileContentError(error)
|
123
|
+
return content_files
|
124
|
+
|
125
|
+
async def execute(self) -> dict:
|
126
|
+
"""Synchronize data from the THIES Center to the cloud."""
|
127
|
+
thies_files = await self.fetch_thies_file_names()
|
128
|
+
cloud_files = await self.fetch_cloud_file_names(folder_name="thies")
|
129
|
+
self.uploading = thies_files - cloud_files
|
130
|
+
if not self.uploading:
|
131
|
+
raise EmptyDataError
|
132
|
+
|
133
|
+
thies_file_contents = await self.fetch_thies_file_content()
|
134
|
+
data = generate_file_content(thies_file_contents)
|
135
|
+
return data
|
rcer_iot_client_pkg-0.1.0/src/rcer_iot_client_pkg/services/epii/utils/update_thies_data_utils.py
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from src.rcer_iot_client_pkg.libs.zero_dependency.utils.datetime_utils import (
|
4
|
+
datetime_to_str,
|
5
|
+
today,
|
6
|
+
)
|
7
|
+
|
8
|
+
|
9
|
+
def generate_file_content(
|
10
|
+
file_contents: dict[str, Any],
|
11
|
+
) -> dict[str, dict[str, int | str]]:
|
12
|
+
return {
|
13
|
+
filename: {
|
14
|
+
"size": len(data),
|
15
|
+
"date": datetime_to_str(today()),
|
16
|
+
}
|
17
|
+
for filename, data in file_contents.items()
|
18
|
+
}
|