saviialib 0.6.1__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.

Potentially problematic release.


This version of saviialib might be problematic. Click here for more details.

Files changed (38) hide show
  1. saviialib-0.6.1/LICENSE +22 -0
  2. saviialib-0.6.1/PKG-INFO +122 -0
  3. saviialib-0.6.1/README.md +100 -0
  4. saviialib-0.6.1/pyproject.toml +44 -0
  5. saviialib-0.6.1/src/saviialib/__init__.py +9 -0
  6. saviialib-0.6.1/src/saviialib/general_types/__init__.py +0 -0
  7. saviialib-0.6.1/src/saviialib/general_types/api/__init__.py +3 -0
  8. saviialib-0.6.1/src/saviialib/general_types/api/update_thies_data_types.py +30 -0
  9. saviialib-0.6.1/src/saviialib/general_types/error_types/__init__.py +0 -0
  10. saviialib-0.6.1/src/saviialib/general_types/error_types/api/__init__.py +0 -0
  11. saviialib-0.6.1/src/saviialib/general_types/error_types/api/update_thies_data_error_types.py +57 -0
  12. saviialib-0.6.1/src/saviialib/general_types/error_types/common/__init__.py +7 -0
  13. saviialib-0.6.1/src/saviialib/general_types/error_types/common/common_types.py +17 -0
  14. saviialib-0.6.1/src/saviialib/libs/ftp_client/__init__.py +4 -0
  15. saviialib-0.6.1/src/saviialib/libs/ftp_client/clients/__init__.py +0 -0
  16. saviialib-0.6.1/src/saviialib/libs/ftp_client/clients/aioftp_client.py +45 -0
  17. saviialib-0.6.1/src/saviialib/libs/ftp_client/ftp_client.py +22 -0
  18. saviialib-0.6.1/src/saviialib/libs/ftp_client/ftp_client_contract.py +13 -0
  19. saviialib-0.6.1/src/saviialib/libs/ftp_client/types/__init__.py +3 -0
  20. saviialib-0.6.1/src/saviialib/libs/ftp_client/types/ftp_client_types.py +18 -0
  21. saviialib-0.6.1/src/saviialib/libs/sharepoint_client/__init__.py +15 -0
  22. saviialib-0.6.1/src/saviialib/libs/sharepoint_client/clients/sharepoint_rest_api.py +135 -0
  23. saviialib-0.6.1/src/saviialib/libs/sharepoint_client/sharepoint_client.py +34 -0
  24. saviialib-0.6.1/src/saviialib/libs/sharepoint_client/sharepoint_client_contract.py +21 -0
  25. saviialib-0.6.1/src/saviialib/libs/sharepoint_client/types/sharepoint_client_types.py +25 -0
  26. saviialib-0.6.1/src/saviialib/libs/zero_dependency/utils/datetime_utils.py +25 -0
  27. saviialib-0.6.1/src/saviialib/services/epii/__init__.py +0 -0
  28. saviialib-0.6.1/src/saviialib/services/epii/api.py +26 -0
  29. saviialib-0.6.1/src/saviialib/services/epii/constants/update_thies_data_constants.py +5 -0
  30. saviialib-0.6.1/src/saviialib/services/epii/controllers/__init__.py +3 -0
  31. saviialib-0.6.1/src/saviialib/services/epii/controllers/types/__init__.py +6 -0
  32. saviialib-0.6.1/src/saviialib/services/epii/controllers/types/update_thies_data_types.py +17 -0
  33. saviialib-0.6.1/src/saviialib/services/epii/controllers/update_thies_data.py +107 -0
  34. saviialib-0.6.1/src/saviialib/services/epii/use_cases/types/__init__.py +7 -0
  35. saviialib-0.6.1/src/saviialib/services/epii/use_cases/types/update_thies_data_types.py +32 -0
  36. saviialib-0.6.1/src/saviialib/services/epii/use_cases/update_thies_data.py +179 -0
  37. saviialib-0.6.1/src/saviialib/services/epii/utils/__init__.py +3 -0
  38. saviialib-0.6.1/src/saviialib/services/epii/utils/update_thies_data_utils.py +21 -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,122 @@
1
+ Metadata-Version: 2.3
2
+ Name: saviialib
3
+ Version: 0.6.1
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
+ # SAVIIA Library
23
+ *Sistema de Administración y Visualización de Información para la Investigación y Análisis*
24
+
25
+ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/pedrozavalat/saviia-lib?style=for-the-badge)](https://github.com/pedrozavalat/saviia-lib/releases)
26
+
27
+
28
+ ## Installation
29
+ This library is designed for use with the SAVIIA Home Assistant Integration. It provides an API to retrieve files from a THIES Data Logger via an FTP server and upload them to a Microsoft SharePoint folder using the SharePoint REST API.
30
+
31
+ ```bash
32
+ pip install saviialib
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Initialize the EPii API Client
38
+ To start using the library, you need to create an `EpiiAPI` client instance:
39
+
40
+ ```python
41
+ from saviialib import EpiiAPI
42
+
43
+ api_client = EpiiAPI()
44
+ ```
45
+
46
+ ### Update THIES Data Logger Files
47
+ The library provides a method to synchronize THIES Data Logger files with the RCER SharePoint client. This method updates the folder containing binary files with meteorological data:
48
+
49
+ ```python
50
+ from saviialib import EpiiUpdateThiesConfig
51
+ import asyncio
52
+
53
+ async def update_thies_data():
54
+ config = EpiiUpdateThiesConfig(
55
+ ftp_port=FTP_PORT,
56
+ ftp_host=FTP_HOST,
57
+ ftp_user=FTP_USER,
58
+ ftp_password=FTP_PASSWORD,
59
+ sharepoint_client_id=SHAREPOINT_CLIENT_ID,
60
+ sharepoint_client_secret=SHAREPOINT_CLIENT_SECRET,
61
+ sharepoint_tenant_id=SHAREPOINT_TENANT_ID,
62
+ sharepoint_tenant_name=SHAREPOINT_TENANT_NAME,
63
+ sharepoint_site_name=SHAREPOINT_SITE_NAME
64
+ )
65
+ response = await api_client.update_thies_data(config)
66
+ return response
67
+
68
+ asyncio.run(update_thies_data())
69
+ ```
70
+
71
+ **Notes:**
72
+ - Store sensitive data like `FTP_PASSWORD`, `FTP_USER`, and SharePoint credentials securely. Use environment variables or a secrets management tool to avoid hardcoding sensitive information in your codebase.
73
+
74
+ ## Development
75
+
76
+ This project includes a `Makefile` to simplify common tasks. Below are the available commands:
77
+
78
+ ### Install Basic Dependencies
79
+ To install the basic dependencies required for the project, run the following command:
80
+
81
+ ```bash
82
+ make install-deps
83
+ ```
84
+
85
+ This will ensure that all necessary libraries and tools are installed for the project to function properly.
86
+
87
+ ### Install Development Requirements
88
+ For setting up a development environment with additional tools and libraries, execute:
89
+
90
+ ```bash
91
+ make dev
92
+ ```
93
+
94
+ This command installs all the dependencies needed for development, including testing and linting tools.
95
+
96
+ ### Run Tests
97
+ To verify that the code is functioning as expected, you can run the test suite using:
98
+
99
+ ```bash
100
+ make test
101
+ ```
102
+
103
+ This will execute all the tests in the project and provide a summary of the results.
104
+
105
+ ### Lint the Code
106
+ To ensure that the code adheres to the project's style guidelines and is free of common errors, run:
107
+
108
+ ```bash
109
+ make lint
110
+ ```
111
+
112
+ This command checks the codebase for linting issues and outputs any problems that need to be addressed.
113
+
114
+ ## Contributing
115
+ If you're interested in contributing to this project, please follow the contributing guidelines. Contributions are welcome and appreciated!
116
+
117
+ 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.
118
+
119
+ ## License
120
+
121
+ `saviialib` was created by Pedro Pablo Zavala Tejos. It is licensed under the terms of the MIT license.
122
+
@@ -0,0 +1,100 @@
1
+ # SAVIIA Library
2
+ *Sistema de Administración y Visualización de Información para la Investigación y Análisis*
3
+
4
+ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/pedrozavalat/saviia-lib?style=for-the-badge)](https://github.com/pedrozavalat/saviia-lib/releases)
5
+
6
+
7
+ ## Installation
8
+ This library is designed for use with the SAVIIA Home Assistant Integration. It provides an API to retrieve files from a THIES Data Logger via an FTP server and upload them to a Microsoft SharePoint folder using the SharePoint REST API.
9
+
10
+ ```bash
11
+ pip install saviialib
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ### Initialize the EPii API Client
17
+ To start using the library, you need to create an `EpiiAPI` client instance:
18
+
19
+ ```python
20
+ from saviialib import EpiiAPI
21
+
22
+ api_client = EpiiAPI()
23
+ ```
24
+
25
+ ### Update THIES Data Logger Files
26
+ The library provides a method to synchronize THIES Data Logger files with the RCER SharePoint client. This method updates the folder containing binary files with meteorological data:
27
+
28
+ ```python
29
+ from saviialib import EpiiUpdateThiesConfig
30
+ import asyncio
31
+
32
+ async def update_thies_data():
33
+ config = EpiiUpdateThiesConfig(
34
+ ftp_port=FTP_PORT,
35
+ ftp_host=FTP_HOST,
36
+ ftp_user=FTP_USER,
37
+ ftp_password=FTP_PASSWORD,
38
+ sharepoint_client_id=SHAREPOINT_CLIENT_ID,
39
+ sharepoint_client_secret=SHAREPOINT_CLIENT_SECRET,
40
+ sharepoint_tenant_id=SHAREPOINT_TENANT_ID,
41
+ sharepoint_tenant_name=SHAREPOINT_TENANT_NAME,
42
+ sharepoint_site_name=SHAREPOINT_SITE_NAME
43
+ )
44
+ response = await api_client.update_thies_data(config)
45
+ return response
46
+
47
+ asyncio.run(update_thies_data())
48
+ ```
49
+
50
+ **Notes:**
51
+ - Store sensitive data like `FTP_PASSWORD`, `FTP_USER`, and SharePoint credentials securely. Use environment variables or a secrets management tool to avoid hardcoding sensitive information in your codebase.
52
+
53
+ ## Development
54
+
55
+ This project includes a `Makefile` to simplify common tasks. Below are the available commands:
56
+
57
+ ### Install Basic Dependencies
58
+ To install the basic dependencies required for the project, run the following command:
59
+
60
+ ```bash
61
+ make install-deps
62
+ ```
63
+
64
+ This will ensure that all necessary libraries and tools are installed for the project to function properly.
65
+
66
+ ### Install Development Requirements
67
+ For setting up a development environment with additional tools and libraries, execute:
68
+
69
+ ```bash
70
+ make dev
71
+ ```
72
+
73
+ This command installs all the dependencies needed for development, including testing and linting tools.
74
+
75
+ ### Run Tests
76
+ To verify that the code is functioning as expected, you can run the test suite using:
77
+
78
+ ```bash
79
+ make test
80
+ ```
81
+
82
+ This will execute all the tests in the project and provide a summary of the results.
83
+
84
+ ### Lint the Code
85
+ To ensure that the code adheres to the project's style guidelines and is free of common errors, run:
86
+
87
+ ```bash
88
+ make lint
89
+ ```
90
+
91
+ This command checks the codebase for linting issues and outputs any problems that need to be addressed.
92
+
93
+ ## Contributing
94
+ If you're interested in contributing to this project, please follow the contributing guidelines. Contributions are welcome and appreciated!
95
+
96
+ 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.
97
+
98
+ ## License
99
+
100
+ `saviialib` was created by Pedro Pablo Zavala Tejos. It is licensed under the terms of the MIT license.
@@ -0,0 +1,44 @@
1
+ [tool.poetry]
2
+ name = "saviialib"
3
+ version = "0.6.1"
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"]
@@ -0,0 +1,9 @@
1
+ # read version from installed package
2
+ from importlib.metadata import version
3
+
4
+ __version__ = version("saviialib")
5
+
6
+ from .services.epii.api import EpiiAPI
7
+ from .general_types.api.update_thies_data_types import EpiiUpdateThiesConfig
8
+
9
+ __all__ = ["EpiiAPI", "EpiiUpdateThiesConfig"]
@@ -0,0 +1,3 @@
1
+ from .update_thies_data_types import EpiiUpdateThiesConfig
2
+
3
+ __all__ = ["EpiiUpdateThiesConfig"]
@@ -0,0 +1,30 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class EpiiUpdateThiesConfig:
6
+ """
7
+ Configuration for Epii API.
8
+
9
+ Attributes:
10
+ ftp_port (int): Port number of the FTP server.
11
+ ftp_host (str): Hostname or IP address of the FTP server.
12
+ ftp_user (str): Username for the FTP server.
13
+ ftp_password (str): Password for the FTP server.
14
+ sharepoint_client_id (str): Client ID for SharePoint authentication.
15
+ sharepoint_client_secret (str): Client secret for SharePoint authentication.
16
+ sharepoint_tenant_id (str): Tenant ID for SharePoint authentication.
17
+ sharepoint_tenant_name (str): Tenant name for SharePoint.
18
+ sharepoint_site_name (str): Site name in SharePoint.
19
+ logger (Logger): Logger object for logging during synchronisation of files from THIES Data Logger
20
+ """
21
+
22
+ ftp_port: int
23
+ ftp_host: str
24
+ ftp_user: str
25
+ ftp_password: str
26
+ sharepoint_client_id: str
27
+ sharepoint_client_secret: str
28
+ sharepoint_tenant_id: str
29
+ sharepoint_tenant_name: str
30
+ sharepoint_site_name: str
@@ -0,0 +1,57 @@
1
+ import json
2
+
3
+
4
+ class ThiesConnectionError(Exception):
5
+ """Raised when unable to connect to the THIES FTP Server"""
6
+
7
+ def __init__(self, *args, reason):
8
+ super().__init__(*args, reason)
9
+ self.reason = reason
10
+
11
+ def __str__(self):
12
+ return "Unable to connect to THIES FTP Server. " + self.reason.__str__()
13
+
14
+
15
+ class ThiesFetchingError(Exception):
16
+ """Raised when no files are found to upload to the server."""
17
+
18
+ def __init__(self, *args, reason):
19
+ super().__init__(*args, reason)
20
+ self.reason = reason
21
+
22
+ def __str__(self):
23
+ return (
24
+ "An error ocurred while retrieving files from THIES FTP Server. "
25
+ + self.reason.__str__()
26
+ )
27
+
28
+
29
+ class SharePointFetchingError(Exception):
30
+ """Raised when there is an error fetching file names from the RCER cloud."""
31
+
32
+ def __init__(self, *args, reason):
33
+ super().__init__(*args, reason)
34
+ self.reason = reason
35
+
36
+ def __str__(self):
37
+ try:
38
+ _, internal_metadata = self.reason.__str__().split(",", 1)
39
+ internal_metadata_dict = json.loads(internal_metadata)
40
+ return internal_metadata_dict["error_description"]
41
+
42
+ except json.decoder.JSONDecodeError:
43
+ return self.reason.__str__()
44
+
45
+
46
+ class SharePointUploadError(Exception):
47
+ """Raised when there is an error uploading files to the Microsoft SharePoint folder."""
48
+
49
+ def __init__(self, *args, reason):
50
+ super().__init__(*args, reason)
51
+ self.reason = reason
52
+
53
+ def __str__(self):
54
+ return (
55
+ "An error occurred while uploading files to the Microsoft SharePoint folder. "
56
+ + self.reason.__str__()
57
+ )
@@ -0,0 +1,7 @@
1
+ from .common_types import (
2
+ EmptyDataError,
3
+ FtpClientError,
4
+ SharepointClientError,
5
+ )
6
+
7
+ __all__ = ["EmptyDataError", "SharepointClientError", "FtpClientError"]
@@ -0,0 +1,17 @@
1
+ class EmptyDataError(Exception):
2
+ def __init__(self, *args, reason):
3
+ super().__init__(*args, reason)
4
+ self.reason = reason
5
+
6
+ def __str__(self):
7
+ return "The data provided is empty. " + self.reason.__str__()
8
+
9
+
10
+ class SharepointClientError(Exception):
11
+ def __str__(self):
12
+ return "SharePoint API REST Client initialization fails."
13
+
14
+
15
+ class FtpClientError(Exception):
16
+ def __str__(self):
17
+ return "Ftp Client initialization fails."
@@ -0,0 +1,4 @@
1
+ from .ftp_client import FTPClient
2
+ from .types import FtpClientInitArgs, FtpListFilesArgs, FtpReadFileArgs
3
+
4
+ __all__ = ["FTPClient", "FtpClientInitArgs", "FtpListFilesArgs", "FtpReadFileArgs"]
@@ -0,0 +1,45 @@
1
+ from aioftp import Client
2
+ from aioftp.errors import StatusCodeError
3
+ from saviialib.libs.ftp_client.ftp_client_contract import (
4
+ FTPClientContract,
5
+ )
6
+ from saviialib.libs.ftp_client.types.ftp_client_types import (
7
+ FtpClientInitArgs,
8
+ FtpListFilesArgs,
9
+ FtpReadFileArgs,
10
+ )
11
+
12
+
13
+ class AioFTPClient(FTPClientContract):
14
+ def __init__(self, args: FtpClientInitArgs) -> None:
15
+ self.host = args.config.ftp_host
16
+ self.port = args.config.ftp_port
17
+ self.password = args.config.ftp_password
18
+ self.user = args.config.ftp_user
19
+ self.client = Client()
20
+
21
+ async def _async_start(self) -> None:
22
+ try:
23
+ await self.client.connect(host=self.host, port=self.port)
24
+ except OSError:
25
+ raise ConnectionRefusedError(
26
+ f"{self.host}:{self.port} isn't active. "
27
+ "Please ensure the server is running and accessible."
28
+ )
29
+ try:
30
+ await self.client.login(user=self.user, password=self.password)
31
+ except StatusCodeError:
32
+ raise ConnectionAbortedError(
33
+ "Authentication failed. Please verify your credentials and try again."
34
+ )
35
+
36
+ async def list_files(self, args: FtpListFilesArgs) -> list[str]:
37
+ await self._async_start()
38
+ return [
39
+ path.name async for path, _ in self.client.list(args.path, recursive=False)
40
+ ]
41
+
42
+ async def read_file(self, args: FtpReadFileArgs) -> bytes:
43
+ await self._async_start()
44
+ async with self.client.download_stream(args.file_path) as stream:
45
+ 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, FtpListFilesArgs, FtpReadFileArgs
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: FtpListFilesArgs) -> list[str]:
19
+ return self.client_obj.list_files(args)
20
+
21
+ def read_file(self, args: FtpReadFileArgs) -> 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 FtpListFilesArgs, FtpReadFileArgs
4
+
5
+
6
+ class FTPClientContract(ABC):
7
+ @abstractmethod
8
+ def list_files(self, args: FtpListFilesArgs) -> list[str]:
9
+ pass
10
+
11
+ @abstractmethod
12
+ def read_file(self, args: FtpReadFileArgs) -> bytes:
13
+ pass
@@ -0,0 +1,3 @@
1
+ from .ftp_client_types import FtpClientInitArgs, FtpListFilesArgs, FtpReadFileArgs
2
+
3
+ __all__ = ["FtpClientInitArgs", "FtpListFilesArgs", "FtpReadFileArgs"]
@@ -0,0 +1,18 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+
5
+ @dataclass
6
+ class FtpClientInitArgs:
7
+ config: Any
8
+ client_name: str = "aioftp_client"
9
+
10
+
11
+ @dataclass
12
+ class FtpListFilesArgs:
13
+ path: str
14
+
15
+
16
+ @dataclass
17
+ class FtpReadFileArgs:
18
+ file_path: str
@@ -0,0 +1,15 @@
1
+ from .sharepoint_client import SharepointClient
2
+ from .types.sharepoint_client_types import (
3
+ SharepointClientInitArgs,
4
+ SpListFilesArgs,
5
+ SpListFoldersArgs,
6
+ SpUploadFileArgs,
7
+ )
8
+
9
+ __all__ = [
10
+ "SharepointClientInitArgs",
11
+ "SharepointClient",
12
+ "SpListFilesArgs",
13
+ "SpListFoldersArgs",
14
+ "SpUploadFileArgs",
15
+ ]
@@ -0,0 +1,135 @@
1
+ from typing import Any
2
+
3
+ from aiohttp import ClientError, ClientSession
4
+ from dotenv import load_dotenv
5
+
6
+ from saviialib.libs.sharepoint_client.sharepoint_client_contract import (
7
+ SharepointClientContract,
8
+ )
9
+ from saviialib.libs.sharepoint_client.types.sharepoint_client_types import (
10
+ SpListFilesArgs,
11
+ SpListFoldersArgs,
12
+ SpUploadFileArgs,
13
+ SharepointClientInitArgs,
14
+ )
15
+
16
+ load_dotenv()
17
+
18
+
19
+ class SharepointRestAPI(SharepointClientContract):
20
+ def __init__(self, args: SharepointClientInitArgs):
21
+ self.session: ClientSession | None = None
22
+ self.base_headers = {}
23
+ self.credentials = {}
24
+ self.base_url = ""
25
+ self.tenant_id = args.config.sharepoint_tenant_id
26
+ self.tenant_name = args.config.sharepoint_tenant_name
27
+ self.client_secret = args.config.sharepoint_client_secret
28
+ self.client_id = args.config.sharepoint_client_id
29
+ self.site_name = args.config.sharepoint_site_name
30
+
31
+ async def _load_form_digest_value(self) -> str:
32
+ try:
33
+ response = await self.session.post("contextinfo")
34
+ response_json = await response.json()
35
+ return response_json["FormDigestValue"]
36
+ except ClientError as error:
37
+ raise ConnectionError(error) from error
38
+
39
+ async def _load_credentials(self) -> dict:
40
+ resource_base = "00000003-0000-0ff1-ce00-000000000000"
41
+ resource = f"{resource_base}/{self.tenant_name}.sharepoint.com@{self.tenant_id}"
42
+ url = f"https://accounts.accesscontrol.windows.net/{self.tenant_id}/tokens/OAuth/2"
43
+ payload = {
44
+ "grant_type": "client_credentials",
45
+ "client_id": f"{self.client_id}@{self.tenant_id}",
46
+ "client_secret": self.client_secret,
47
+ "resource": resource,
48
+ }
49
+ headers = {
50
+ "Content-Type": "application/x-www-form-urlencoded",
51
+ }
52
+
53
+ async with ClientSession() as session:
54
+ # Load access token
55
+ response = await session.post(url, data=payload, headers=headers)
56
+ if response.status != 200:
57
+ raise ClientError(
58
+ f"Failed to fetch credentials: {response.status}, {await response.text()}"
59
+ )
60
+ response_json = await response.json()
61
+
62
+ return {
63
+ "access_token": response_json["access_token"],
64
+ }
65
+
66
+ async def __aenter__(self) -> "SharepointRestAPI":
67
+ try:
68
+ self.credentials = await self._load_credentials()
69
+ site_url = f"https://{self.tenant_name}.sharepoint.com"
70
+
71
+ self.base_headers = {
72
+ "Authorization": f"Bearer {self.credentials['access_token']}",
73
+ "Accept": "application/json",
74
+ "Content-Type": "application/json",
75
+ }
76
+ self.base_url = f"{site_url}/sites/{self.site_name}/_api/"
77
+ self.session = ClientSession(
78
+ headers=self.base_headers, base_url=self.base_url
79
+ )
80
+ return self
81
+ except ClientError as error:
82
+ raise ConnectionError(error)
83
+
84
+ async def __aexit__(
85
+ self, _exc_type: type[BaseException], _exc_val: BaseException, _exc_tb: Any
86
+ ) -> None:
87
+ await self.session.close()
88
+
89
+ async def list_files(self, args: SpListFilesArgs) -> list:
90
+ try:
91
+ folder_relative_url = (
92
+ f"GetFolderByServerRelativeUrl('{args.folder_relative_url}')"
93
+ )
94
+ endpoint = f"web/{folder_relative_url}/Files"
95
+ response = await self.session.get(endpoint.lstrip("/"))
96
+ response.raise_for_status()
97
+ response_json = await response.json()
98
+ return response_json
99
+ except ClientError as error:
100
+ raise ConnectionError(error) from error
101
+
102
+ async def list_folders(self, args: SpListFoldersArgs) -> list:
103
+ try:
104
+ folder_relative_url = (
105
+ f"GetFolderByServerRelativeUrl('{args.folder_relative_url}')"
106
+ )
107
+ endpoint = f"web/{folder_relative_url}/Folder"
108
+ response = await self.session.get(endpoint.lstrip("/"))
109
+ response.raise_for_status()
110
+ return await response.json()
111
+ except ClientError as error:
112
+ raise ConnectionError(error) from error
113
+
114
+ async def upload_file(self, args: SpUploadFileArgs) -> dict:
115
+ try:
116
+ # Load form digest value
117
+ form_digest_value = await self._load_form_digest_value()
118
+ headers = {
119
+ **self.base_headers,
120
+ "X-RequestDigest": form_digest_value,
121
+ "Content-Type": "application/octet-stream",
122
+ }
123
+ # Upload the file in the requested folder
124
+ folder_relative_url = (
125
+ f"GetFolderByServerRelativeUrl('{args.folder_relative_url}')"
126
+ )
127
+ data = args.file_content
128
+
129
+ endpoint = f"web/{folder_relative_url}/Files/add(url='{args.file_name}',overwrite=true)"
130
+ response = await self.session.post(endpoint, data=data, headers=headers)
131
+
132
+ response.raise_for_status()
133
+ return await response.json()
134
+ except ClientError as error:
135
+ raise ConnectionError(error) from error
@@ -0,0 +1,34 @@
1
+ from .clients.sharepoint_rest_api import SharepointRestAPI
2
+ from .sharepoint_client_contract import SharepointClientContract
3
+ from .types.sharepoint_client_types import (
4
+ SharepointClientInitArgs,
5
+ SpListFilesArgs,
6
+ SpListFoldersArgs,
7
+ SpUploadFileArgs,
8
+ )
9
+
10
+
11
+ class SharepointClient(SharepointClientContract):
12
+ CLIENTS = {"sharepoint_rest_api"}
13
+
14
+ def __init__(self, args: SharepointClientInitArgs):
15
+ if args.client_name not in SharepointClient.CLIENTS:
16
+ msg = f"Unsupported client {args.client_name}"
17
+ raise KeyError(msg)
18
+ elif args.client_name == "sharepoint_rest_api":
19
+ self.client_obj = SharepointRestAPI(args)
20
+
21
+ async def __aenter__(self):
22
+ return await self.client_obj.__aenter__()
23
+
24
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
25
+ await self.client_obj.__aexit__(exc_type, exc_val, exc_tb)
26
+
27
+ async def list_files(self, args: SpListFilesArgs) -> list:
28
+ return await self.client_obj.list_files(args)
29
+
30
+ async def list_folders(self, args: SpListFoldersArgs) -> list:
31
+ return await self.client_obj.list_files(args)
32
+
33
+ async def upload_file(self, args: SpUploadFileArgs) -> dict:
34
+ return await self.client_obj.upload_file(args)
@@ -0,0 +1,21 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from .types.sharepoint_client_types import (
4
+ SpListFilesArgs,
5
+ SpListFoldersArgs,
6
+ SpUploadFileArgs,
7
+ )
8
+
9
+
10
+ class SharepointClientContract(ABC):
11
+ @abstractmethod
12
+ async def list_files(self, args: SpListFilesArgs) -> list:
13
+ pass
14
+
15
+ @abstractmethod
16
+ async def list_folders(self, args: SpListFoldersArgs) -> list:
17
+ pass
18
+
19
+ @abstractmethod
20
+ async def upload_file(self, args: SpUploadFileArgs) -> dict:
21
+ pass
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+
5
+ @dataclass
6
+ class SharepointClientInitArgs:
7
+ config: Any
8
+ client_name: str = "sharepoint_rest_api"
9
+
10
+
11
+ @dataclass
12
+ class SpListFilesArgs:
13
+ folder_relative_url: str
14
+
15
+
16
+ @dataclass
17
+ class SpListFoldersArgs:
18
+ folder_relative_url: str
19
+
20
+
21
+ @dataclass
22
+ class SpUploadFileArgs:
23
+ folder_relative_url: str
24
+ file_name: str
25
+ file_content: bytes = bytes()
@@ -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)
@@ -0,0 +1,26 @@
1
+ from typing import Any, Dict
2
+
3
+ from .controllers.types.update_thies_data_types import UpdateThiesDataControllerInput
4
+ from .controllers.update_thies_data import UpdateThiesDataController
5
+ from saviialib.general_types.api.update_thies_data_types import (
6
+ EpiiUpdateThiesConfig,
7
+ )
8
+
9
+
10
+ class EpiiAPI:
11
+ """
12
+ EpiiAPI is a service class that provides methods to interact with Patagonia Center system.
13
+ """
14
+
15
+ async def update_thies_data(self, config: EpiiUpdateThiesConfig) -> Dict[str, Any]:
16
+ """
17
+ This method establishes a connection to an FTP server using the provided
18
+ credentials and updates data related to THIES Data Logger.
19
+ Args:
20
+ config (EpiiUpdateThiesConfig): configuration class for FTP Server and Microsoft SharePoint credentials.
21
+ Returns:
22
+ response (dict): A dictionary representation of the API response.
23
+ """
24
+ controller = UpdateThiesDataController(UpdateThiesDataControllerInput(config))
25
+ response = await controller.execute()
26
+ return response.__dict__
@@ -0,0 +1,5 @@
1
+ SHAREPOINT_BASE_URL = "/sites/uc365_CentrosyEstacionesRegionalesUC/Shared%20Documents/General/Test_Raspberry/THIES"
2
+ SHAREPOINT_THIES_FOLDERS = ["AVG", "EXT"]
3
+
4
+ FTP_SERVER_PATH_AVG_FILES = "ftp/thies/BINFILES/ARCH_AV1"
5
+ FTP_SERVER_PATH_EXT_FILES = "ftp/thies/BINFILES/ARCH_EX1"
@@ -0,0 +1,3 @@
1
+ from .update_thies_data import UpdateThiesDataController
2
+
3
+ __all__ = ["UpdateThiesDataController"]
@@ -0,0 +1,6 @@
1
+ from .update_thies_data_types import (
2
+ UpdateThiesDataControllerInput,
3
+ UpdateThiesDataControllerOutput,
4
+ )
5
+
6
+ __all__ = ["UpdateThiesDataControllerInput", "UpdateThiesDataControllerOutput"]
@@ -0,0 +1,17 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict
3
+ from saviialib.general_types.api.update_thies_data_types import (
4
+ EpiiUpdateThiesConfig,
5
+ )
6
+
7
+
8
+ @dataclass
9
+ class UpdateThiesDataControllerInput:
10
+ config: EpiiUpdateThiesConfig
11
+
12
+
13
+ @dataclass
14
+ class UpdateThiesDataControllerOutput:
15
+ message: str
16
+ status: int
17
+ metadata: Dict[str, str] = field(default_factory=dict)
@@ -0,0 +1,107 @@
1
+ from http import HTTPStatus
2
+
3
+ from saviialib.general_types.error_types.api.update_thies_data_error_types import (
4
+ SharePointFetchingError,
5
+ ThiesConnectionError,
6
+ ThiesFetchingError,
7
+ SharePointUploadError,
8
+ )
9
+ from saviialib.general_types.error_types.common.common_types import (
10
+ EmptyDataError,
11
+ FtpClientError,
12
+ SharepointClientError,
13
+ )
14
+ from saviialib.services.epii.controllers.types.update_thies_data_types import (
15
+ UpdateThiesDataControllerInput,
16
+ UpdateThiesDataControllerOutput,
17
+ )
18
+ from saviialib.services.epii.use_cases.types import (
19
+ UpdateThiesDataUseCaseInput,
20
+ SharepointConfig,
21
+ FtpClientConfig,
22
+ )
23
+ from saviialib.services.epii.use_cases.update_thies_data import (
24
+ UpdateThiesDataUseCase,
25
+ )
26
+
27
+
28
+ class UpdateThiesDataController:
29
+ def __init__(self, input: UpdateThiesDataControllerInput):
30
+ self.use_case = UpdateThiesDataUseCase(
31
+ UpdateThiesDataUseCaseInput(
32
+ ftp_config=FtpClientConfig(
33
+ ftp_host=input.config.ftp_host,
34
+ ftp_password=input.config.ftp_password,
35
+ ftp_port=input.config.ftp_port,
36
+ ftp_user=input.config.ftp_user,
37
+ ),
38
+ sharepoint_config=SharepointConfig(
39
+ sharepoint_client_id=input.config.sharepoint_client_id,
40
+ sharepoint_client_secret=input.config.sharepoint_client_secret,
41
+ sharepoint_site_name=input.config.sharepoint_site_name,
42
+ sharepoint_tenant_name=input.config.sharepoint_tenant_name,
43
+ sharepoint_tenant_id=input.config.sharepoint_tenant_id,
44
+ ),
45
+ )
46
+ )
47
+
48
+ async def execute(self) -> UpdateThiesDataControllerOutput:
49
+ try:
50
+ data = await self.use_case.execute()
51
+ return UpdateThiesDataControllerOutput(
52
+ message="THIES was synced successfully",
53
+ status=HTTPStatus.OK.value,
54
+ metadata={"data": data},
55
+ )
56
+ except EmptyDataError:
57
+ return UpdateThiesDataControllerOutput(
58
+ message="No files to upload", status=HTTPStatus.NO_CONTENT.value
59
+ )
60
+
61
+ except (AttributeError, NameError, ValueError) as error:
62
+ return UpdateThiesDataControllerOutput(
63
+ message="An unexpected error occurred during use case initialization.",
64
+ status=HTTPStatus.BAD_REQUEST.value,
65
+ metadata={"error": error.__str__()},
66
+ )
67
+ except FtpClientError as error:
68
+ return UpdateThiesDataControllerOutput(
69
+ message="Ftp Client initialization fails.",
70
+ status=HTTPStatus.BAD_REQUEST.value,
71
+ metadata={"error": error.__str__()},
72
+ )
73
+
74
+ except SharepointClientError as error:
75
+ return UpdateThiesDataControllerOutput(
76
+ message="Sharepoint Client initialization fails.",
77
+ status=HTTPStatus.INTERNAL_SERVER_ERROR.value,
78
+ metadata={"error": error.__str__()},
79
+ )
80
+
81
+ except SharePointFetchingError as error:
82
+ return UpdateThiesDataControllerOutput(
83
+ message="An error occurred while retrieving file names from Microsoft SharePoint",
84
+ status=HTTPStatus.BAD_REQUEST.value,
85
+ metadata={"error": error.__str__()},
86
+ )
87
+
88
+ except SharePointUploadError as error:
89
+ return UpdateThiesDataControllerOutput(
90
+ message="An error oucrred while uploading files to RCER Cloud",
91
+ status=HTTPStatus.BAD_REQUEST.value,
92
+ metadata={"error": error.__str__()},
93
+ )
94
+
95
+ except ThiesFetchingError as error:
96
+ return UpdateThiesDataControllerOutput(
97
+ message="An error ocurred while retrieving file names from THIES FTP Server.",
98
+ status=HTTPStatus.NO_CONTENT.value,
99
+ metadata={"error": error.__str__()},
100
+ )
101
+
102
+ except ThiesConnectionError as error:
103
+ return UpdateThiesDataControllerOutput(
104
+ message="Unable to connect to THIES Data Logger FTP Server.",
105
+ status=HTTPStatus.INTERNAL_SERVER_ERROR.value,
106
+ metadata={"error": error.__str__()},
107
+ )
@@ -0,0 +1,7 @@
1
+ from .update_thies_data_types import (
2
+ FtpClientConfig,
3
+ SharepointConfig,
4
+ UpdateThiesDataUseCaseInput,
5
+ )
6
+
7
+ __all__ = ["UpdateThiesDataUseCaseInput", "FtpClientConfig", "SharepointConfig"]
@@ -0,0 +1,32 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict
3
+
4
+
5
+ @dataclass
6
+ class FtpClientConfig:
7
+ ftp_host: str
8
+ ftp_port: int
9
+ ftp_user: str
10
+ ftp_password: str
11
+
12
+
13
+ @dataclass
14
+ class SharepointConfig:
15
+ sharepoint_client_id: str
16
+ sharepoint_client_secret: str
17
+ sharepoint_tenant_id: str
18
+ sharepoint_tenant_name: str
19
+ sharepoint_site_name: str
20
+
21
+
22
+ @dataclass
23
+ class UpdateThiesDataUseCaseInput:
24
+ ftp_config: FtpClientConfig
25
+ sharepoint_config: SharepointConfig
26
+
27
+
28
+ @dataclass
29
+ class UpdateThiesDataUseCaseOutput:
30
+ message: str
31
+ status: int = 0
32
+ metadata: Dict[str, str] = field(default_factory=dict)
@@ -0,0 +1,179 @@
1
+ from dotenv import load_dotenv
2
+
3
+ import saviialib.services.epii.constants.update_thies_data_constants as c
4
+ from saviialib.general_types.error_types.api.update_thies_data_error_types import (
5
+ SharePointFetchingError,
6
+ SharePointUploadError,
7
+ ThiesConnectionError,
8
+ ThiesFetchingError,
9
+ )
10
+ from saviialib.general_types.error_types.common import (
11
+ EmptyDataError,
12
+ FtpClientError,
13
+ SharepointClientError,
14
+ )
15
+ from saviialib.libs.ftp_client import (
16
+ FTPClient,
17
+ FtpClientInitArgs,
18
+ FtpListFilesArgs,
19
+ FtpReadFileArgs,
20
+ )
21
+ from saviialib.libs.sharepoint_client import (
22
+ SharepointClient,
23
+ SharepointClientInitArgs,
24
+ SpListFilesArgs,
25
+ SpUploadFileArgs,
26
+ )
27
+ from saviialib.services.epii.use_cases.types import (
28
+ FtpClientConfig,
29
+ SharepointConfig,
30
+ UpdateThiesDataUseCaseInput,
31
+ )
32
+ from saviialib.services.epii.utils import (
33
+ parse_execute_response,
34
+ )
35
+
36
+ load_dotenv()
37
+
38
+
39
+ class UpdateThiesDataUseCase:
40
+ def __init__(self, input: UpdateThiesDataUseCaseInput):
41
+ self.sharepoint_client = self._initialize_sharepoint_client(
42
+ input.sharepoint_config
43
+ )
44
+ self.thies_ftp_client = self._initialize_thies_ftp_client(input.ftp_config)
45
+ self.uploading = set()
46
+
47
+ def _initialize_sharepoint_client(
48
+ self, config: SharepointConfig
49
+ ) -> SharepointClient:
50
+ """Initialize the HTTP client."""
51
+ try:
52
+ return SharepointClient(
53
+ SharepointClientInitArgs(config, client_name="sharepoint_rest_api")
54
+ )
55
+ except ConnectionError as error:
56
+ raise SharepointClientError(error)
57
+
58
+ def _initialize_thies_ftp_client(self, config: FtpClientConfig) -> FTPClient:
59
+ """Initialize the FTP client."""
60
+ try:
61
+ return FTPClient(FtpClientInitArgs(config, client_name="aioftp_client"))
62
+ except RuntimeError as error:
63
+ raise FtpClientError(error)
64
+
65
+ async def fetch_cloud_file_names(self) -> set[str]:
66
+ """Fetch file names from the RCER cloud."""
67
+
68
+ try:
69
+ cloud_files = set()
70
+ async with self.sharepoint_client:
71
+ for folder in c.SHAREPOINT_THIES_FOLDERS:
72
+ args = SpListFilesArgs(
73
+ folder_relative_url=f"{c.SHAREPOINT_BASE_URL}/{folder}"
74
+ )
75
+ response = await self.sharepoint_client.list_files(args)
76
+ cloud_files.update(
77
+ {f"{folder}_{item['Name']}" for item in response["value"]}
78
+ )
79
+ return cloud_files
80
+ except ConnectionError as error:
81
+ raise SharePointFetchingError(reason=error)
82
+
83
+ async def fetch_thies_file_names(self) -> set[str]:
84
+ """Fetch file names from the THIES FTP server."""
85
+ try:
86
+ avg_files = await self.thies_ftp_client.list_files(
87
+ FtpListFilesArgs(path=c.FTP_SERVER_PATH_AVG_FILES)
88
+ )
89
+ ext_files = await self.thies_ftp_client.list_files(
90
+ FtpListFilesArgs(path=c.FTP_SERVER_PATH_EXT_FILES)
91
+ )
92
+ return {f"AVG_{name}" for name in avg_files} | {
93
+ f"EXT_{name}" for name in ext_files
94
+ }
95
+ except ConnectionRefusedError as error:
96
+ raise ThiesConnectionError(reason=error)
97
+ except ConnectionAbortedError as error:
98
+ raise ThiesFetchingError(reason=error)
99
+
100
+ async def fetch_thies_file_content(self) -> dict[str, bytes]:
101
+ """Fetch the content of files from the THIES FTP server."""
102
+ try:
103
+ content_files = {}
104
+ for file in self.uploading:
105
+ origin, filename = file.split("_", 1)
106
+ file_path = (
107
+ f"{c.FTP_SERVER_PATH_AVG_FILES}/{filename}"
108
+ if origin == "AVG"
109
+ else f"{c.FTP_SERVER_PATH_EXT_FILES}/{filename}"
110
+ )
111
+ content = await self.thies_ftp_client.read_file(
112
+ FtpReadFileArgs(file_path)
113
+ )
114
+ content_files[file] = content # Save the file with its prefix
115
+ return content_files
116
+ except ConnectionRefusedError as error:
117
+ raise ThiesConnectionError(reason=error)
118
+ except ConnectionAbortedError as error:
119
+ raise ThiesFetchingError(reason=error)
120
+
121
+ async def upload_thies_files_to_sharepoint(
122
+ self, files: dict
123
+ ) -> dict[str, list[str]]:
124
+ """Upload files to SharePoint and categorize the results."""
125
+ upload_results = {"failed_files": [], "overwritten_files": [], "new_files": []}
126
+
127
+ async with self.sharepoint_client:
128
+ for file, file_content in files.items():
129
+ try:
130
+ folder, file_name = file.split("_", 1)
131
+ args = SpUploadFileArgs(
132
+ folder_relative_url=f"{c.SHAREPOINT_BASE_URL}/{folder}",
133
+ file_content=file_content,
134
+ file_name=file_name,
135
+ )
136
+ response = await self.sharepoint_client.upload_file(args)
137
+
138
+ if response.get("Exists", False):
139
+ upload_results["overwritten_files"].append(file)
140
+ else:
141
+ upload_results["new_files"].append(file)
142
+
143
+ except ConnectionError as error:
144
+ upload_results["failed_files"].append(
145
+ f"{file} (Error: {str(error)})"
146
+ )
147
+
148
+ if upload_results["failed_files"]:
149
+ raise SharePointUploadError(
150
+ reason="Files failed to upload: "
151
+ + ", ".join(upload_results["failed_files"])
152
+ )
153
+
154
+ return upload_results
155
+
156
+ async def execute(self) -> dict:
157
+ """Synchronize data from the THIES Center to the cloud."""
158
+ try:
159
+ thies_files = await self.fetch_thies_file_names()
160
+ except RuntimeError as error:
161
+ raise FtpClientError(error)
162
+ try:
163
+ cloud_files = await self.fetch_cloud_file_names()
164
+ except RuntimeError as error:
165
+ raise SharepointClient(error)
166
+
167
+ self.uploading = thies_files - cloud_files
168
+ if not self.uploading:
169
+ raise EmptyDataError(reason="No files to upload.")
170
+
171
+ # Fetch the content of the files to be uploaded from THIES FTP Server
172
+ thies_fetched_files = await self.fetch_thies_file_content()
173
+
174
+ # Upload the fetched files to SharePoint and gather statistics
175
+ upload_statistics = await self.upload_thies_files_to_sharepoint(
176
+ thies_fetched_files
177
+ )
178
+
179
+ return parse_execute_response(thies_fetched_files, upload_statistics)
@@ -0,0 +1,3 @@
1
+ from .update_thies_data_utils import parse_execute_response
2
+
3
+ __all__ = ["parse_execute_response"]
@@ -0,0 +1,21 @@
1
+ from typing import Any
2
+
3
+ from saviialib.libs.zero_dependency.utils.datetime_utils import (
4
+ datetime_to_str,
5
+ today,
6
+ )
7
+
8
+
9
+ def parse_execute_response(
10
+ thies_fetched_files: dict[str, Any], upload_statistics: dict[str, Any]
11
+ ) -> dict[str, dict[str, int | str]]:
12
+ return {
13
+ **upload_statistics,
14
+ "processed_files": {
15
+ filename: {
16
+ "file_size": len(data),
17
+ "processed_date": datetime_to_str(today()),
18
+ }
19
+ for filename, data in thies_fetched_files.items()
20
+ },
21
+ }