saviialib 0.6.1__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 saviialib might be problematic. Click here for more details.
- saviialib/__init__.py +9 -0
- saviialib/general_types/__init__.py +0 -0
- saviialib/general_types/api/__init__.py +3 -0
- saviialib/general_types/api/update_thies_data_types.py +30 -0
- saviialib/general_types/error_types/__init__.py +0 -0
- saviialib/general_types/error_types/api/__init__.py +0 -0
- saviialib/general_types/error_types/api/update_thies_data_error_types.py +57 -0
- saviialib/general_types/error_types/common/__init__.py +7 -0
- saviialib/general_types/error_types/common/common_types.py +17 -0
- saviialib/libs/ftp_client/__init__.py +4 -0
- saviialib/libs/ftp_client/clients/__init__.py +0 -0
- saviialib/libs/ftp_client/clients/aioftp_client.py +45 -0
- saviialib/libs/ftp_client/ftp_client.py +22 -0
- saviialib/libs/ftp_client/ftp_client_contract.py +13 -0
- saviialib/libs/ftp_client/types/__init__.py +3 -0
- saviialib/libs/ftp_client/types/ftp_client_types.py +18 -0
- saviialib/libs/sharepoint_client/__init__.py +15 -0
- saviialib/libs/sharepoint_client/clients/sharepoint_rest_api.py +135 -0
- saviialib/libs/sharepoint_client/sharepoint_client.py +34 -0
- saviialib/libs/sharepoint_client/sharepoint_client_contract.py +21 -0
- saviialib/libs/sharepoint_client/types/sharepoint_client_types.py +25 -0
- saviialib/libs/zero_dependency/utils/datetime_utils.py +25 -0
- saviialib/services/epii/__init__.py +0 -0
- saviialib/services/epii/api.py +26 -0
- saviialib/services/epii/constants/update_thies_data_constants.py +5 -0
- saviialib/services/epii/controllers/__init__.py +3 -0
- saviialib/services/epii/controllers/types/__init__.py +6 -0
- saviialib/services/epii/controllers/types/update_thies_data_types.py +17 -0
- saviialib/services/epii/controllers/update_thies_data.py +107 -0
- saviialib/services/epii/use_cases/types/__init__.py +7 -0
- saviialib/services/epii/use_cases/types/update_thies_data_types.py +32 -0
- saviialib/services/epii/use_cases/update_thies_data.py +179 -0
- saviialib/services/epii/utils/__init__.py +3 -0
- saviialib/services/epii/utils/update_thies_data_utils.py +21 -0
- saviialib-0.6.1.dist-info/LICENSE +22 -0
- saviialib-0.6.1.dist-info/METADATA +122 -0
- saviialib-0.6.1.dist-info/RECORD +38 -0
- saviialib-0.6.1.dist-info/WHEEL +4 -0
saviialib/__init__.py
ADDED
|
@@ -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"]
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
@@ -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,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."
|
|
File without changes
|
|
@@ -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,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)
|
|
File without changes
|
|
@@ -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,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,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,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
|
+
}
|
|
@@ -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
|
+
[](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,38 @@
|
|
|
1
|
+
saviialib/__init__.py,sha256=_pFtnu8wiICE81UO5XRSgCwMwldbJ4wSeYO5A1P6fPM,278
|
|
2
|
+
saviialib/general_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
saviialib/general_types/api/__init__.py,sha256=Dmy5H7Mm4n-ewj3dC1wzM5o-379Ebeb9hnBOqq8M4IE,96
|
|
4
|
+
saviialib/general_types/api/update_thies_data_types.py,sha256=1ZTjHSSfmhOp9wAHOMHfQxoibiNKN05PdpRrXUcd2JQ,1077
|
|
5
|
+
saviialib/general_types/error_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
saviialib/general_types/error_types/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
saviialib/general_types/error_types/api/update_thies_data_error_types.py,sha256=YkbMXzg3B2fD-L-sh6f__6uZQ9nkvN6rUacchcPpJn8,1690
|
|
8
|
+
saviialib/general_types/error_types/common/__init__.py,sha256=yOBLZbt64Ki9Q0IJ0tMAubgq7PtrQ7XQ3RgtAzyOjiE,170
|
|
9
|
+
saviialib/general_types/error_types/common/common_types.py,sha256=n5yuw-gVtkrtNfmaZ83ZkYxYHGl4jynOLUB9C8Tr32w,474
|
|
10
|
+
saviialib/libs/ftp_client/__init__.py,sha256=dW2Yutgc7mJJJzgKLhWKXMgQ6KIWJYfFa1sGpjHH5xU,191
|
|
11
|
+
saviialib/libs/ftp_client/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
saviialib/libs/ftp_client/clients/aioftp_client.py,sha256=gcPhLX1EE8_iiAGQ1eTWxzJ-qd3yjP9Ug1LJPibqGn8,1613
|
|
13
|
+
saviialib/libs/ftp_client/ftp_client.py,sha256=RhWnGwfU8SN0tRqfDc3u6NKDj9qla4cOI2IANyb-S2s,820
|
|
14
|
+
saviialib/libs/ftp_client/ftp_client_contract.py,sha256=2x1MPZNFVw3l-sVd65Z6kZYkXlk_8isH_ykzRZB9Cv8,326
|
|
15
|
+
saviialib/libs/ftp_client/types/__init__.py,sha256=syfwf9feP4QK7fkCTfl4j8l11ic-jHtfi1DE2chaWbs,155
|
|
16
|
+
saviialib/libs/ftp_client/types/ftp_client_types.py,sha256=e4SmYkewldulaD8ms2q75zVgLFXyBxBqoa_L-IQOmso,256
|
|
17
|
+
saviialib/libs/sharepoint_client/__init__.py,sha256=v7h-cNsK-BaPp-hTU3NWRNXYRD9ztU-hsCk0eNRPIKA,334
|
|
18
|
+
saviialib/libs/sharepoint_client/clients/sharepoint_rest_api.py,sha256=6TGw2svGZ9QiABJdiUNzTIjmla3f70-hVxPK0cOZEVc,5252
|
|
19
|
+
saviialib/libs/sharepoint_client/sharepoint_client.py,sha256=lzzRk-5YzScNL15TzHp8dAcdhiAtRVCfY7L2GEqoccM,1246
|
|
20
|
+
saviialib/libs/sharepoint_client/sharepoint_client_contract.py,sha256=xqNHzCjp7GvUGGUox9YTJj2QJgTc5819t2etOk8X26o,485
|
|
21
|
+
saviialib/libs/sharepoint_client/types/sharepoint_client_types.py,sha256=OmPlCJ9rLrAFBeG6aDp5cxMiQ5BZlDyGVx5S4GN4aqg,414
|
|
22
|
+
saviialib/libs/zero_dependency/utils/datetime_utils.py,sha256=kD38wC087H3jwTIgrntBajE55cR2ioo_ftPUHiyGs_M,751
|
|
23
|
+
saviialib/services/epii/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
saviialib/services/epii/api.py,sha256=0CMxvl8AoDUqfE5bFxf7bnpvTcFYVRBtEXoRjxSghNk,1061
|
|
25
|
+
saviialib/services/epii/constants/update_thies_data_constants.py,sha256=38f2WKV-zBO9O-CV8mh3tW3MFMiaJrrv4_FfQiyHApc,275
|
|
26
|
+
saviialib/services/epii/controllers/__init__.py,sha256=mCdGgKGDgGxCtRoiZN9Rki-fTOyOuJWw9e7festpQYA,98
|
|
27
|
+
saviialib/services/epii/controllers/types/__init__.py,sha256=xzky-oTSojLNkWETp_k8a4dcXYvYSQY0VhWo23Yhb8U,195
|
|
28
|
+
saviialib/services/epii/controllers/types/update_thies_data_types.py,sha256=6r63NUTskYpaRrTG7V7yv2va0geHyONfnLxQu1XGEU0,389
|
|
29
|
+
saviialib/services/epii/controllers/update_thies_data.py,sha256=8XeWJNfhwGCJ3Le5YKOnXRz9wKhVOTcfhqoCIIRG5PQ,4343
|
|
30
|
+
saviialib/services/epii/use_cases/types/__init__.py,sha256=u6fyodOEJE2j6FMqJux40Xf9ccYAi-UUYxqT-Kzc0kE,199
|
|
31
|
+
saviialib/services/epii/use_cases/types/update_thies_data_types.py,sha256=C0TU50KKYodpaX87OnG0MnHyGY4gRzmluUHk-esCEVU,635
|
|
32
|
+
saviialib/services/epii/use_cases/update_thies_data.py,sha256=uBpk6i-snOpXL88to-64KHoJ5_ubdGpauAoUnlBzbPk,6734
|
|
33
|
+
saviialib/services/epii/utils/__init__.py,sha256=cYt2tvq65_OMjFaqb8-CCC7IGCQgFd4ziEUWJV7s1iY,98
|
|
34
|
+
saviialib/services/epii/utils/update_thies_data_utils.py,sha256=EpjYWXqyHxJ-dO3MHhdXp-rGV7WyUckeFko-nnfnNac,555
|
|
35
|
+
saviialib-0.6.1.dist-info/LICENSE,sha256=NWpf6b38xgBWPBo5HZsCbdfp9hZSliEbRqWQgm0fkOo,1076
|
|
36
|
+
saviialib-0.6.1.dist-info/METADATA,sha256=hMk2t2m8HMuwhslnLqlgRaUNMLkzBnAIC0pbzz3w77A,4134
|
|
37
|
+
saviialib-0.6.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
38
|
+
saviialib-0.6.1.dist-info/RECORD,,
|