rcer-iot-client-pkg 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. rcer_iot_client_pkg/general_types/error_types/api/update_thies_data_error_types.py +20 -9
  2. rcer_iot_client_pkg/general_types/error_types/common/__init__.py +2 -2
  3. rcer_iot_client_pkg/general_types/error_types/common/common_types.py +7 -3
  4. rcer_iot_client_pkg/libs/ftp_client/__init__.py +2 -2
  5. rcer_iot_client_pkg/libs/ftp_client/clients/aioftp_client.py +16 -8
  6. rcer_iot_client_pkg/libs/ftp_client/ftp_client.py +3 -3
  7. rcer_iot_client_pkg/libs/ftp_client/ftp_client_contract.py +3 -3
  8. rcer_iot_client_pkg/libs/ftp_client/types/__init__.py +2 -2
  9. rcer_iot_client_pkg/libs/ftp_client/types/ftp_client_types.py +2 -2
  10. rcer_iot_client_pkg/libs/sharepoint_client/__init__.py +15 -0
  11. rcer_iot_client_pkg/libs/sharepoint_client/clients/sharepoint_rest_api.py +134 -0
  12. rcer_iot_client_pkg/libs/sharepoint_client/sharepoint_client.py +34 -0
  13. rcer_iot_client_pkg/libs/sharepoint_client/sharepoint_client_contract.py +21 -0
  14. rcer_iot_client_pkg/libs/sharepoint_client/types/sharepoint_client_types.py +23 -0
  15. rcer_iot_client_pkg/services/epii/api.py +17 -2
  16. rcer_iot_client_pkg/services/epii/constants/update_thies_data_constants.py +5 -0
  17. rcer_iot_client_pkg/services/epii/controllers/update_thies_data.py +23 -8
  18. rcer_iot_client_pkg/services/epii/use_cases/types/update_thies_data_types.py +1 -1
  19. rcer_iot_client_pkg/services/epii/use_cases/update_thies_data.py +50 -46
  20. rcer_iot_client_pkg/services/epii/utils/__init__.py +2 -2
  21. rcer_iot_client_pkg/services/epii/utils/update_thies_data_utils.py +1 -1
  22. {rcer_iot_client_pkg-0.3.1.dist-info → rcer_iot_client_pkg-0.4.0.dist-info}/METADATA +34 -1
  23. {rcer_iot_client_pkg-0.3.1.dist-info → rcer_iot_client_pkg-0.4.0.dist-info}/RECORD +25 -20
  24. rcer_iot_client_pkg/services/epii/use_cases/constants.py +0 -4
  25. {rcer_iot_client_pkg-0.3.1.dist-info → rcer_iot_client_pkg-0.4.0.dist-info}/LICENSE +0 -0
  26. {rcer_iot_client_pkg-0.3.1.dist-info → rcer_iot_client_pkg-0.4.0.dist-info}/WHEEL +0 -0
@@ -1,19 +1,30 @@
1
- class ThiesUploadEmptyError(Exception):
2
- """Raised when no files are found to upload to the server."""
1
+ class ThiesConnectionError(Exception):
2
+ """Raised when unable to connect to the THIES FTP Server"""
3
+
4
+ def __init__(self, *args, reason):
5
+ super().__init__(*args, reason)
6
+ self.reason = reason
3
7
 
4
8
  def __str__(self):
5
- return "No files were found to upload."
9
+ return "Unable to connect to THIES FTP Server. " + self.reason.__str__()
6
10
 
7
11
 
8
- class FetchCloudFileNamesError(Exception):
9
- """Raised when there is an error fetching file names from the RCER cloud."""
12
+ class ThiesFetchingError(Exception):
13
+ """Raised when no files are found to upload to the server."""
14
+
15
+ def __init__(self, *args, reason):
16
+ super().__init__(*args, reason)
17
+ self.reason = reason
10
18
 
11
19
  def __str__(self):
12
- return "An error occurred while retrieving file names from the RCER cloud"
20
+ return (
21
+ "An error ocurred while retrieving files from THIES FTP Server. "
22
+ + self.reason.__str__()
23
+ )
13
24
 
14
25
 
15
- class FetchThiesFileContentError(Exception):
16
- """Raised when there is an error fetching the content of a Thies file."""
26
+ class SharePointFetchingError(Exception):
27
+ """Raised when there is an error fetching file names from the RCER cloud."""
17
28
 
18
29
  def __str__(self):
19
- return "An error occurred while retrieving the content of a Thies file"
30
+ return "An error occurred while retrieving file names from the RCER cloud"
@@ -1,7 +1,7 @@
1
1
  from .common_types import (
2
2
  EmptyDataError,
3
3
  FtpClientError,
4
- HttpClientError,
4
+ SharepointClientError,
5
5
  )
6
6
 
7
- __all__ = ["EmptyDataError", "HttpClientError", "FtpClientError"]
7
+ __all__ = ["EmptyDataError", "SharepointClientError", "FtpClientError"]
@@ -1,11 +1,15 @@
1
1
  class EmptyDataError(Exception):
2
+ def __init__(self, *args, reason):
3
+ super().__init__(*args, reason)
4
+ self.reason = reason
5
+
2
6
  def __str__(self):
3
- return "The data provided is empty."
7
+ return "The data provided is empty. " + self.reason.__str__()
4
8
 
5
9
 
6
- class HttpClientError(Exception):
10
+ class SharepointClientError(Exception):
7
11
  def __str__(self):
8
- return "Http Client initialization fails."
12
+ return "SharePoint API REST Client initialization fails."
9
13
 
10
14
 
11
15
  class FtpClientError(Exception):
@@ -1,4 +1,4 @@
1
1
  from .ftp_client import FTPClient
2
- from .types import FtpClientInitArgs, ListFilesArgs, ReadFileArgs
2
+ from .types import FtpClientInitArgs, FtpListFilesArgs, FtpReadFileArgs
3
3
 
4
- __all__ = ["FTPClient", "FtpClientInitArgs", "ListFilesArgs", "ReadFileArgs"]
4
+ __all__ = ["FTPClient", "FtpClientInitArgs", "FtpListFilesArgs", "FtpReadFileArgs"]
@@ -1,12 +1,12 @@
1
1
  from aioftp import Client
2
-
2
+ from aioftp.errors import StatusCodeError
3
3
  from rcer_iot_client_pkg.libs.ftp_client.ftp_client_contract import (
4
4
  FTPClientContract,
5
5
  )
6
6
  from rcer_iot_client_pkg.libs.ftp_client.types.ftp_client_types import (
7
7
  FtpClientInitArgs,
8
- ListFilesArgs,
9
- ReadFileArgs,
8
+ FtpListFilesArgs,
9
+ FtpReadFileArgs,
10
10
  )
11
11
 
12
12
 
@@ -19,19 +19,27 @@ class AioFTPClient(FTPClientContract):
19
19
  self.client = Client()
20
20
 
21
21
  async def _async_start(self) -> None:
22
- try:
22
+ try:
23
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:
24
30
  await self.client.login(user=self.user, password=self.password)
25
- except Exception:
26
- raise RuntimeError("Unexpected error occurred while trying to connect to the FTP server")
31
+ except StatusCodeError:
32
+ raise ConnectionAbortedError(
33
+ "Authentication failed. Please verify your credentials and try again."
34
+ )
27
35
 
28
- async def list_files(self, args: ListFilesArgs) -> list[str]:
36
+ async def list_files(self, args: FtpListFilesArgs) -> list[str]:
29
37
  await self._async_start()
30
38
  return [
31
39
  path.name async for path, _ in self.client.list(args.path, recursive=False)
32
40
  ]
33
41
 
34
- async def read_file(self, args: ReadFileArgs) -> bytes:
42
+ async def read_file(self, args: FtpReadFileArgs) -> bytes:
35
43
  await self._async_start()
36
44
  async with self.client.download_stream(args.file_path) as stream:
37
45
  return await stream.read()
@@ -1,6 +1,6 @@
1
1
  from .clients.aioftp_client import AioFTPClient
2
2
  from .ftp_client_contract import FTPClientContract
3
- from .types.ftp_client_types import FtpClientInitArgs, ListFilesArgs, ReadFileArgs
3
+ from .types.ftp_client_types import FtpClientInitArgs, FtpListFilesArgs, FtpReadFileArgs
4
4
 
5
5
 
6
6
  class FTPClient(FTPClientContract):
@@ -15,8 +15,8 @@ class FTPClient(FTPClientContract):
15
15
  self.client_obj = AioFTPClient(args)
16
16
  self.client_name = args.client_name
17
17
 
18
- def list_files(self, args: ListFilesArgs) -> list[str]:
18
+ def list_files(self, args: FtpListFilesArgs) -> list[str]:
19
19
  return self.client_obj.list_files(args)
20
20
 
21
- def read_file(self, args: ReadFileArgs) -> bytes:
21
+ def read_file(self, args: FtpReadFileArgs) -> bytes:
22
22
  return self.client_obj.read_file(args)
@@ -1,13 +1,13 @@
1
1
  from abc import ABC, abstractmethod
2
2
 
3
- from .types.ftp_client_types import ListFilesArgs, ReadFileArgs
3
+ from .types.ftp_client_types import FtpListFilesArgs, FtpReadFileArgs
4
4
 
5
5
 
6
6
  class FTPClientContract(ABC):
7
7
  @abstractmethod
8
- def list_files(self, args: ListFilesArgs) -> list[str]:
8
+ def list_files(self, args: FtpListFilesArgs) -> list[str]:
9
9
  pass
10
10
 
11
11
  @abstractmethod
12
- def read_file(self, args: ReadFileArgs) -> bytes:
12
+ def read_file(self, args: FtpReadFileArgs) -> bytes:
13
13
  pass
@@ -1,3 +1,3 @@
1
- from .ftp_client_types import FtpClientInitArgs, ListFilesArgs, ReadFileArgs
1
+ from .ftp_client_types import FtpClientInitArgs, FtpListFilesArgs, FtpReadFileArgs
2
2
 
3
- __all__ = ["FtpClientInitArgs", "ListFilesArgs", "ReadFileArgs"]
3
+ __all__ = ["FtpClientInitArgs", "FtpListFilesArgs", "FtpReadFileArgs"]
@@ -11,10 +11,10 @@ class FtpClientInitArgs:
11
11
 
12
12
 
13
13
  @dataclass
14
- class ListFilesArgs:
14
+ class FtpListFilesArgs:
15
15
  path: str
16
16
 
17
17
 
18
18
  @dataclass
19
- class ReadFileArgs:
19
+ class FtpReadFileArgs:
20
20
  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,134 @@
1
+ import os
2
+ from typing import Any
3
+
4
+ from aiohttp import ClientError, ClientSession
5
+ from dotenv import load_dotenv
6
+
7
+ from rcer_iot_client_pkg.libs.sharepoint_client.sharepoint_client_contract import (
8
+ SharepointClientContract,
9
+ )
10
+ from rcer_iot_client_pkg.libs.sharepoint_client.types.sharepoint_client_types import (
11
+ SpListFilesArgs,
12
+ SpListFoldersArgs,
13
+ SpUploadFileArgs,
14
+ )
15
+
16
+ load_dotenv()
17
+
18
+
19
+ class SharepointRestAPI(SharepointClientContract):
20
+ def __init__(self):
21
+ self.session: ClientSession | None = None
22
+ self.base_headers = {}
23
+ self.credentials = {}
24
+ self.base_url = ""
25
+
26
+ async def _load_form_digest_value(self) -> str:
27
+ try:
28
+ response = await self.session.post("contextinfo")
29
+ response_json = await response.json()
30
+ return response_json["FormDigestValue"]
31
+ except ClientError as error:
32
+ raise ConnectionError(error) from error
33
+
34
+ async def _load_credentials(self) -> dict:
35
+ tenant_id = os.getenv("TENANT_ID")
36
+ client_id = os.getenv("CLIENT_ID")
37
+ client_secret = os.getenv("CLIENT_SECRET")
38
+ resource_base = "00000003-0000-0ff1-ce00-000000000000"
39
+ resource = (
40
+ f"{resource_base}/{os.getenv('TENANT_NAME')}.sharepoint.com@{tenant_id}"
41
+ )
42
+ url = f"https://accounts.accesscontrol.windows.net/{tenant_id}/tokens/OAuth/2"
43
+ payload = {
44
+ "grant_type": "client_credentials",
45
+ "client_id": f"{client_id}@{tenant_id}",
46
+ "client_secret": 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
+ self.credentials = await self._load_credentials()
68
+ site_url = f"https://{os.getenv('TENANT_NAME')}.sharepoint.com"
69
+ site_name = os.getenv("SITE_NAME")
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/{site_name}/_api/"
77
+ self.session = ClientSession(headers=self.base_headers, base_url=self.base_url)
78
+ return self
79
+
80
+ async def __aexit__(
81
+ self, _exc_type: type[BaseException], _exc_val: BaseException, _exc_tb: Any
82
+ ) -> None:
83
+ await self.session.close()
84
+
85
+ async def list_files(self, args: SpListFilesArgs) -> list:
86
+ try:
87
+ folder_relative_url = (
88
+ f"GetFolderByServerRelativeUrl('{args.folder_relative_url}')"
89
+ )
90
+ endpoint = f"web/{folder_relative_url}/Files"
91
+ response = await self.session.get(endpoint.lstrip("/"))
92
+ response.raise_for_status()
93
+ response_json = await response.json()
94
+ return response_json
95
+ except ClientError as error:
96
+ raise ConnectionError(error) from error
97
+
98
+ async def list_folders(self, args: SpListFoldersArgs) -> list:
99
+ try:
100
+ folder_relative_url = (
101
+ f"GetFolderByServerRelativeUrl('{args.folder_relative_url}')"
102
+ )
103
+ endpoint = f"web/{folder_relative_url}/Folder"
104
+ response = await self.session.get(endpoint.lstrip("/"))
105
+ response.raise_for_status()
106
+ return await response.json()
107
+ except ClientError as error:
108
+ raise ConnectionError(error) from error
109
+
110
+ async def upload_file(self, args: SpUploadFileArgs) -> dict:
111
+ try:
112
+ # Load form digest value
113
+ form_digest_value = await self._load_form_digest_value()
114
+ headers = {
115
+ **self.base_headers,
116
+ "X-RequestDigest": form_digest_value,
117
+ "Content-Type": "application/octet-stream",
118
+ }
119
+ # Upload the file in the requested folder
120
+ folder_relative_url = (
121
+ f"GetFolderByServerRelativeUrl('{args.folder_relative_url}')"
122
+ )
123
+ # Read the file
124
+ source_file_path = os.path.basename(args.file_path)
125
+ with open(source_file_path, "rb") as file:
126
+ data = file.read()
127
+
128
+ endpoint = f"web/{folder_relative_url}/Files/add(url='{source_file_path}',overwrite=false)"
129
+ response = await self.session.post(endpoint, data=data, headers=headers)
130
+
131
+ response.raise_for_status()
132
+ return await response.json()
133
+ except ClientError as error:
134
+ 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()
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 self.client_obj.list_files(args)
32
+
33
+ async def upload_file(self, args: SpUploadFileArgs) -> dict:
34
+ return 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,23 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class SharepointClientInitArgs:
6
+ client_name: str = "sharepoint_rest_api"
7
+
8
+
9
+ @dataclass
10
+ class SpListFilesArgs:
11
+ folder_relative_url: str
12
+
13
+
14
+ @dataclass
15
+ class SpListFoldersArgs:
16
+ folder_relative_url: str
17
+
18
+
19
+ @dataclass
20
+ class SpUploadFileArgs:
21
+ file_path: str
22
+ folder_relative_url: str
23
+ file_content: bytes = bytes()
@@ -1,17 +1,32 @@
1
- from typing import Dict
1
+ from typing import Dict, Any
2
2
 
3
3
  from .controllers.types.update_thies_data_types import UpdateThiesDataControllerInput
4
4
  from .controllers.update_thies_data import UpdateThiesDataController
5
5
 
6
6
 
7
7
  class EpiiAPI:
8
+ """
9
+ EpiiAPI is a service class that provides methods to interact with Patagonia Center system.
10
+ """
11
+
8
12
  async def update_thies_data(
9
13
  self,
10
14
  ftp_port: int,
11
15
  ftp_host: str,
12
16
  ftp_password: str,
13
17
  ftp_user: str,
14
- ) -> Dict[str, any]:
18
+ ) -> Dict[str, Any]:
19
+ """
20
+ This method establishes a connection to an FTP server using the provided
21
+ credentials and updates data related to THIES Data Logger.
22
+ Args:
23
+ ftp_port (int): The port number of the FTP server.
24
+ ftp_host (str): The hostname or IP address of the FTP server.
25
+ ftp_password (str): The password for the FTP server.
26
+ ftp_user (str): The username for the FTP server.
27
+ Returns:
28
+ response (dict): A dictionary representation of the API response.
29
+ """
15
30
  controller = UpdateThiesDataController(
16
31
  UpdateThiesDataControllerInput(
17
32
  ftp_port=ftp_port,
@@ -0,0 +1,5 @@
1
+ SHAREPOINT_BASE_URL = "/sites/uc365_CentrosyEstacionesRegionalesUC/Shared Documents/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"
@@ -1,12 +1,14 @@
1
1
  from http import HTTPStatus
2
2
 
3
3
  from rcer_iot_client_pkg.general_types.error_types.api.update_thies_data_error_types import (
4
- FetchCloudFileNamesError,
5
- ThiesUploadEmptyError,
4
+ SharePointFetchingError,
5
+ ThiesConnectionError,
6
+ ThiesFetchingError,
6
7
  )
7
8
  from rcer_iot_client_pkg.general_types.error_types.common.common_types import (
9
+ EmptyDataError,
8
10
  FtpClientError,
9
- HttpClientError,
11
+ SharepointClientError,
10
12
  )
11
13
  from rcer_iot_client_pkg.services.epii.controllers.types.update_thies_data_types import (
12
14
  UpdateThiesDataControllerInput,
@@ -34,6 +36,11 @@ class UpdateThiesDataController:
34
36
  status=HTTPStatus.OK.value,
35
37
  metadata={"data": data},
36
38
  )
39
+ except EmptyDataError:
40
+ return UpdateThiesDataControllerOutput(
41
+ message="No files to upload", status=HTTPStatus.NO_CONTENT
42
+ )
43
+
37
44
  except (AttributeError, NameError, ValueError) as error:
38
45
  return UpdateThiesDataControllerOutput(
39
46
  message="An unexpected error occurred during use case initialization.",
@@ -47,22 +54,30 @@ class UpdateThiesDataController:
47
54
  metadata={"error": error.__str__()},
48
55
  )
49
56
 
50
- except HttpClientError as error:
57
+ except SharepointClientError as error:
51
58
  return UpdateThiesDataControllerOutput(
52
- message="Http Client initialization fails.",
59
+ message="Sharepoint Client initialization fails.",
53
60
  status=HTTPStatus.BAD_REQUEST.value,
54
61
  metadata={"error": error.__str__()},
55
62
  )
56
63
 
57
- except FetchCloudFileNamesError as error:
64
+ except SharePointFetchingError as error:
58
65
  return UpdateThiesDataControllerOutput(
59
66
  message="An error occurred while retrieving file names from the RCER cloud",
60
67
  status=HTTPStatus.INTERNAL_SERVER_ERROR.value,
61
68
  metadata={"error": error.__str__()},
62
69
  )
63
- except ThiesUploadEmptyError as error:
70
+
71
+ except ThiesFetchingError as error:
64
72
  return UpdateThiesDataControllerOutput(
65
- message="No files were found to upload.",
73
+ message="An error ocurred while retrieving file names from THIES FTP Server.",
66
74
  status=HTTPStatus.NO_CONTENT.value,
67
75
  metadata={"error": error.__str__()},
68
76
  )
77
+
78
+ except ThiesConnectionError as error:
79
+ return UpdateThiesDataControllerOutput(
80
+ message="Unable to connect to THIES Data Logger FTP Server.",
81
+ status=HTTPStatus.INTERNAL_SERVER_ERROR.value,
82
+ metadata={"error": error.__str__()},
83
+ )
@@ -5,7 +5,7 @@ from typing import Dict
5
5
  @dataclass
6
6
  class UpdateThiesDataUseCaseInput:
7
7
  ftp_host: str
8
- ftp_port: str
8
+ ftp_port: int
9
9
  ftp_user: str
10
10
  ftp_password: str
11
11
 
@@ -1,32 +1,32 @@
1
1
  from dotenv import load_dotenv
2
2
 
3
- import rcer_iot_client_pkg.services.epii.use_cases.constants as c
3
+ import rcer_iot_client_pkg.services.epii.constants.update_thies_data_constants as c
4
4
  from rcer_iot_client_pkg.general_types.error_types.api.update_thies_data_error_types import (
5
- FetchCloudFileNamesError,
6
- FetchThiesFileContentError,
7
- ThiesUploadEmptyError,
5
+ SharePointFetchingError,
6
+ ThiesConnectionError,
7
+ ThiesFetchingError,
8
8
  )
9
9
  from rcer_iot_client_pkg.general_types.error_types.common import (
10
10
  EmptyDataError,
11
11
  FtpClientError,
12
- HttpClientError,
13
- )
14
- from rcer_iot_client_pkg.libs.async_http_client import (
15
- AsyncHTTPClient,
16
- AsyncHttpClientInitArgs,
17
- GetArgs,
12
+ SharepointClientError,
18
13
  )
19
14
  from rcer_iot_client_pkg.libs.ftp_client import (
20
15
  FTPClient,
21
16
  FtpClientInitArgs,
22
- ListFilesArgs,
23
- ReadFileArgs,
17
+ FtpListFilesArgs,
18
+ FtpReadFileArgs,
19
+ )
20
+ from rcer_iot_client_pkg.libs.sharepoint_client import (
21
+ SharepointClient,
22
+ SharepointClientInitArgs,
23
+ SpListFilesArgs,
24
24
  )
25
25
  from rcer_iot_client_pkg.services.epii.use_cases.types import (
26
26
  UpdateThiesDataUseCaseInput,
27
27
  )
28
28
  from rcer_iot_client_pkg.services.epii.utils import (
29
- generate_file_content,
29
+ parse_execute_response,
30
30
  )
31
31
 
32
32
  load_dotenv()
@@ -42,18 +42,14 @@ class UpdateThiesDataUseCase:
42
42
  self.thies_ftp_client = self._initialize_thies_ftp_client()
43
43
  self.uploading = set()
44
44
 
45
- def _initialize_sharepoint_client(self) -> AsyncHTTPClient:
45
+ def _initialize_sharepoint_client(self) -> SharepointClient:
46
46
  """Initialize the HTTP client."""
47
47
  try:
48
- return AsyncHTTPClient(
49
- AsyncHttpClientInitArgs(
50
- client_name="aiohttp_client",
51
- access_token="temporal-token",
52
- base_url="https://graph.microsoft.com/v1.0/",
53
- )
48
+ return SharepointClient(
49
+ SharepointClientInitArgs(client_name="sharepoint_rest_api")
54
50
  )
55
51
  except ConnectionError as error:
56
- raise HttpClientError(error)
52
+ raise SharepointClientError(error)
57
53
 
58
54
  def _initialize_thies_ftp_client(self) -> FTPClient:
59
55
  """Initialize the FTP client."""
@@ -70,55 +66,61 @@ class UpdateThiesDataUseCase:
70
66
  except RuntimeError as error:
71
67
  raise FtpClientError(error)
72
68
 
73
- async def fetch_cloud_file_names(self, folder_name: str) -> set[str]:
69
+ async def fetch_cloud_file_names(self) -> set[str]:
74
70
  """Fetch file names from the RCER cloud."""
71
+
75
72
  try:
76
73
  cloud_files = set()
77
74
  async with self.sharepoint_client:
78
- for file_type in c.FILE_TYPES:
79
- destination_path = f"Onedrive_UC/noveno-semestre/IPRE-RCER/{folder_name}/{file_type}"
80
- endpoint = f"drives/{c.DRIVE_ID}/root:/{destination_path}:/children"
81
- response = await self.sharepoint_client.get(
82
- GetArgs(endpoint=endpoint)
75
+ for folder in c.SHAREPOINT_THIES_FOLDERS:
76
+ args = SpListFilesArgs(
77
+ folder_relative_url=f"{c.SHAREPOINT_BASE_URL}/{folder}"
83
78
  )
79
+ response = await self.sharepoint_client.list_files(args)
84
80
  cloud_files.update(
85
- {f"{file_type}_{item['name']}" for item in response["value"]}
81
+ {f"{folder}_{item['Name']}" for item in response["value"]}
86
82
  )
87
83
  return cloud_files
88
84
  except ConnectionError as error:
89
- raise FetchCloudFileNamesError(error)
85
+ raise SharePointFetchingError(reason=error)
90
86
 
91
87
  async def fetch_thies_file_names(self) -> set[str]:
92
88
  """Fetch file names from the THIES FTP server."""
93
89
  try:
94
90
  avg_files = await self.thies_ftp_client.list_files(
95
- ListFilesArgs(path=c.PATH_AVG_FILES)
91
+ FtpListFilesArgs(path=c.FTP_SERVER_PATH_AVG_FILES)
96
92
  )
97
93
  ext_files = await self.thies_ftp_client.list_files(
98
- ListFilesArgs(path=c.PATH_EXT_FILES)
94
+ FtpListFilesArgs(path=c.FTP_SERVER_PATH_EXT_FILES)
99
95
  )
100
96
  return {f"AVG_{name}" for name in avg_files} | {
101
97
  f"EXT_{name}" for name in ext_files
102
98
  }
103
- except ConnectionError:
104
- raise ThiesUploadEmptyError
99
+ except ConnectionRefusedError as error:
100
+ raise ThiesConnectionError(reason=error)
101
+ except ConnectionAbortedError as error:
102
+ raise ThiesFetchingError(reason=error)
105
103
 
106
104
  async def fetch_thies_file_content(self) -> dict[str, bytes]:
107
105
  """Fetch the content of files from the THIES FTP server."""
108
- content_files = {}
109
- for file in self.uploading:
110
- try:
106
+ try:
107
+ content_files = {}
108
+ for file in self.uploading:
111
109
  origin, filename = file.split("_", 1)
112
110
  file_path = (
113
- f"{c.PATH_AVG_FILES}/{filename}"
111
+ f"{c.FTP_SERVER_PATH_AVG_FILES}/{filename}"
114
112
  if origin == "AVG"
115
- else f"{c.PATH_EXT_FILES}/{filename}"
113
+ else f"{c.FTP_SERVER_PATH_EXT_FILES}/{filename}"
114
+ )
115
+ content = await self.thies_ftp_client.read_file(
116
+ FtpReadFileArgs(file_path)
116
117
  )
117
- content = await self.thies_ftp_client.read_file(ReadFileArgs(file_path))
118
118
  content_files[filename] = content
119
- except ConnectionError as error:
120
- raise FetchThiesFileContentError(error)
121
- return content_files
119
+ return content_files
120
+ except ConnectionRefusedError as error:
121
+ raise ThiesConnectionError(reason=error)
122
+ except ConnectionAbortedError as error:
123
+ raise ThiesFetchingError(reason=error)
122
124
 
123
125
  async def execute(self) -> dict:
124
126
  """Synchronize data from the THIES Center to the cloud."""
@@ -126,12 +128,14 @@ class UpdateThiesDataUseCase:
126
128
  thies_files = await self.fetch_thies_file_names()
127
129
  except RuntimeError as error:
128
130
  raise FtpClientError(error)
129
-
130
- cloud_files = await self.fetch_cloud_file_names(folder_name="thies")
131
+ try:
132
+ cloud_files = await self.fetch_cloud_file_names()
133
+ except RuntimeError as error:
134
+ raise SharepointClient(error)
131
135
  self.uploading = thies_files - cloud_files
132
136
  if not self.uploading:
133
- raise EmptyDataError
137
+ raise EmptyDataError(reason="No files to upload.")
134
138
 
135
139
  thies_file_contents = await self.fetch_thies_file_content()
136
- data = generate_file_content(thies_file_contents)
140
+ data = parse_execute_response(thies_file_contents)
137
141
  return data
@@ -1,3 +1,3 @@
1
- from .update_thies_data_utils import generate_file_content
1
+ from .update_thies_data_utils import parse_execute_response
2
2
 
3
- __all__ = ["generate_file_content"]
3
+ __all__ = ["parse_execute_response"]
@@ -6,7 +6,7 @@ from rcer_iot_client_pkg.libs.zero_dependency.utils.datetime_utils import (
6
6
  )
7
7
 
8
8
 
9
- def generate_file_content(
9
+ def parse_execute_response(
10
10
  file_contents: dict[str, Any],
11
11
  ) -> dict[str, dict[str, int | str]]:
12
12
  return {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: rcer_iot_client_pkg
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: A client library for IoT projects in the RCER initiative
5
5
  License: MIT
6
6
  Author: pedropablozavalat
@@ -30,6 +30,39 @@ This library provides a robust and efficient client for interacting with IoT dev
30
30
  pip install rcer_iot_client_pkg
31
31
  ```
32
32
 
33
+ ## Usage
34
+
35
+ ### Initialize the EPii API Client
36
+ To start using the library, you need to create an `EpiiAPI` client instance:
37
+
38
+ ```python
39
+ from rcer_iot_client_pkg import EpiiAPI
40
+
41
+ api_client = EpiiAPI()
42
+ ```
43
+
44
+ ### Update THIES Data Logger Files
45
+ 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:
46
+
47
+ ```python
48
+ import asyncio
49
+
50
+ async def update_thies_data():
51
+ response = await api_client.update_thies_data(
52
+ ftp_port=PORT,
53
+ ftp_host=LOCAL_HOST,
54
+ ftp_password=PASSWORD,
55
+ ftp_user=USER
56
+ )
57
+ return response
58
+
59
+ asyncio.run(update_thies_data())
60
+ ```
61
+
62
+ **Notes:**
63
+ - Store sensitive data like `PASSWORD` and `USER` securely, e.g., in environment variables or a secrets file.
64
+ - Ensure `asyncio` is installed to run concurrent code with `EpiiAPI` methods.
65
+
33
66
  ## Development
34
67
 
35
68
  This project includes a `Makefile` to simplify common tasks. Below are the available commands:
@@ -4,9 +4,9 @@ rcer_iot_client_pkg/general_types/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
4
4
  rcer_iot_client_pkg/general_types/api/update_thies_data_types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  rcer_iot_client_pkg/general_types/error_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  rcer_iot_client_pkg/general_types/error_types/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- rcer_iot_client_pkg/general_types/error_types/api/update_thies_data_error_types.py,sha256=pbkIZxKTGtTOSVVcOVQ-mINN5mY8JYt5PeFqsyAF-Sg,640
8
- rcer_iot_client_pkg/general_types/error_types/common/__init__.py,sha256=qb0Z01TjUlOSrC9W40z011XxabrjGQN8UddO8upr8KQ,158
9
- rcer_iot_client_pkg/general_types/error_types/common/common_types.py,sha256=w25vajcnX24zIezMG3EjG32dDouV51p-iXtNKz8qjD4,319
7
+ rcer_iot_client_pkg/general_types/error_types/api/update_thies_data_error_types.py,sha256=lDVcK7RrqT4s0Bzxa5nEyIsNee93GaIRCyPV9k0s4-o,928
8
+ rcer_iot_client_pkg/general_types/error_types/common/__init__.py,sha256=yOBLZbt64Ki9Q0IJ0tMAubgq7PtrQ7XQ3RgtAzyOjiE,170
9
+ rcer_iot_client_pkg/general_types/error_types/common/common_types.py,sha256=n5yuw-gVtkrtNfmaZ83ZkYxYHGl4jynOLUB9C8Tr32w,474
10
10
  rcer_iot_client_pkg/libs/async_http_client/__init__.py,sha256=LF1KEh5Dhu0j9qZhSBGOsD-PWUFKEvS2oE0eUCyNLd0,279
11
11
  rcer_iot_client_pkg/libs/async_http_client/async_http_client.py,sha256=wlOvjThS8wnpyqdcY7HbeEx1X3eVNKxJ8DuCJtx0wFk,1135
12
12
  rcer_iot_client_pkg/libs/async_http_client/async_http_client_contract.py,sha256=PCoTt4R6vFwjvyOTETQ8jEw1uqzAQC3PYiIlg_d7iRI,816
@@ -14,27 +14,32 @@ rcer_iot_client_pkg/libs/async_http_client/clients/__init__.py,sha256=47DEQpj8HB
14
14
  rcer_iot_client_pkg/libs/async_http_client/clients/aiohttp_client.py,sha256=exJ7yq_9ukKoGhdyEwUEe8N5syvEsv2vsQ5eJcQbKSc,1837
15
15
  rcer_iot_client_pkg/libs/async_http_client/types/__init__.py,sha256=TnJOBj5R6rVA5XAiId5dpQM6KjMIfi8KJkCCav7n930,154
16
16
  rcer_iot_client_pkg/libs/async_http_client/types/async_http_client_types.py,sha256=d-_rHQ21ajTr5AI66fH3-cpJzF8VhSqG0BHbVrlwND8,329
17
- rcer_iot_client_pkg/libs/ftp_client/__init__.py,sha256=6c6C3_hnZVApYKlRVOlCeEPH8RPnQ22c5Fqqb-hVKp0,179
17
+ rcer_iot_client_pkg/libs/ftp_client/__init__.py,sha256=dW2Yutgc7mJJJzgKLhWKXMgQ6KIWJYfFa1sGpjHH5xU,191
18
18
  rcer_iot_client_pkg/libs/ftp_client/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- rcer_iot_client_pkg/libs/ftp_client/clients/aioftp_client.py,sha256=cI65J230Ptew3zSsMdWdZ_9EBBDft-k9mlYwcM6410s,1269
20
- rcer_iot_client_pkg/libs/ftp_client/ftp_client.py,sha256=sYaq_ilO1DlDHHMFz4Gt4t7oTx3hXFHQJx9WCoqG7M4,808
21
- rcer_iot_client_pkg/libs/ftp_client/ftp_client_contract.py,sha256=sISJSMzkCBTdRZT9xpspzQBvo03d80mC4XdmbEd_EwY,314
22
- rcer_iot_client_pkg/libs/ftp_client/types/__init__.py,sha256=_HaW5-nKNvxfgf7tiGoAI_bZewzneHE9DY_3niwFRKw,143
23
- rcer_iot_client_pkg/libs/ftp_client/types/ftp_client_types.py,sha256=uNECnvKpc8z-WFE1C-EaYypDqprGmyG25sKDfTvONT4,276
19
+ rcer_iot_client_pkg/libs/ftp_client/clients/aioftp_client.py,sha256=b0YYNXOTlYqsxwmxrcCqb__xy7CBv6PO5-WiZkOioRk,1589
20
+ rcer_iot_client_pkg/libs/ftp_client/ftp_client.py,sha256=RhWnGwfU8SN0tRqfDc3u6NKDj9qla4cOI2IANyb-S2s,820
21
+ rcer_iot_client_pkg/libs/ftp_client/ftp_client_contract.py,sha256=2x1MPZNFVw3l-sVd65Z6kZYkXlk_8isH_ykzRZB9Cv8,326
22
+ rcer_iot_client_pkg/libs/ftp_client/types/__init__.py,sha256=syfwf9feP4QK7fkCTfl4j8l11ic-jHtfi1DE2chaWbs,155
23
+ rcer_iot_client_pkg/libs/ftp_client/types/ftp_client_types.py,sha256=4ywi5UROTR5TOaApPrm-JyV2PeTvD-cv5K_lL4Fj9AA,282
24
+ rcer_iot_client_pkg/libs/sharepoint_client/__init__.py,sha256=v7h-cNsK-BaPp-hTU3NWRNXYRD9ztU-hsCk0eNRPIKA,334
25
+ rcer_iot_client_pkg/libs/sharepoint_client/clients/sharepoint_rest_api.py,sha256=fjmpP7ityjBRBLpok0GYQMrMNIhrCwVjIvLKbBdsQkk,5095
26
+ rcer_iot_client_pkg/libs/sharepoint_client/sharepoint_client.py,sha256=cTHQan7vMCJ2In3D1s1Nizvnt7K5Y8n87WiYcmVdjhk,1230
27
+ rcer_iot_client_pkg/libs/sharepoint_client/sharepoint_client_contract.py,sha256=xqNHzCjp7GvUGGUox9YTJj2QJgTc5819t2etOk8X26o,485
28
+ rcer_iot_client_pkg/libs/sharepoint_client/types/sharepoint_client_types.py,sha256=NTV5kL1xEOr5tUZOJMkr3gFTwSXRG1V9PkmHtntyQk0,375
24
29
  rcer_iot_client_pkg/libs/zero_dependency/utils/datetime_utils.py,sha256=kD38wC087H3jwTIgrntBajE55cR2ioo_ftPUHiyGs_M,751
25
30
  rcer_iot_client_pkg/services/epii/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- rcer_iot_client_pkg/services/epii/api.py,sha256=7tPzQAMwaN5q3M8Eb7NpyT4LT48nfVrHZSRO2Ji-bsM,708
31
+ rcer_iot_client_pkg/services/epii/api.py,sha256=lqoyBR6ub6BLqwOPR_kvtG8z80KqUNpJD8Yl6VT7z34,1369
32
+ rcer_iot_client_pkg/services/epii/constants/update_thies_data_constants.py,sha256=XeBNsQaTsc0mylQegB4Wf9ECIo0i1Fk-BurWm84WDqE,273
27
33
  rcer_iot_client_pkg/services/epii/controllers/__init__.py,sha256=mCdGgKGDgGxCtRoiZN9Rki-fTOyOuJWw9e7festpQYA,98
28
34
  rcer_iot_client_pkg/services/epii/controllers/types/__init__.py,sha256=xzky-oTSojLNkWETp_k8a4dcXYvYSQY0VhWo23Yhb8U,195
29
35
  rcer_iot_client_pkg/services/epii/controllers/types/update_thies_data_types.py,sha256=0zbNgjln7PXQRjFPAbiNV9n_cXV1H1-XtXMCC_VA4kE,336
30
- rcer_iot_client_pkg/services/epii/controllers/update_thies_data.py,sha256=_TeHGWoIx-QC3KTfI0_o6rimkx9v_7hOpjSvSGWz4Ls,2698
31
- rcer_iot_client_pkg/services/epii/use_cases/constants.py,sha256=fslc2XtEBs2bwVL1pfXXeDnJw35YLYz35ApXwatplH8,202
36
+ rcer_iot_client_pkg/services/epii/controllers/update_thies_data.py,sha256=Si-oGHyunp8lgxvjdZjRizAJuf2uoq5b6QD1dRHYVuM,3272
32
37
  rcer_iot_client_pkg/services/epii/use_cases/types/__init__.py,sha256=c2RRUbjWq2PIqYW8DcxmYDg0gLZ74kmHh3BkeQCVii0,108
33
- rcer_iot_client_pkg/services/epii/use_cases/types/update_thies_data_types.py,sha256=Rt76-f3tjJpg5VL8sraxXYVhXG_GHlnDBKotqCFuS8o,334
34
- rcer_iot_client_pkg/services/epii/use_cases/update_thies_data.py,sha256=UNq-I6WR8MKA6wenWVGi21hTimeholU4xMG8dhI9Am4,5058
35
- rcer_iot_client_pkg/services/epii/utils/__init__.py,sha256=cAhC0EiJkIF9WxXfst93v1AZ-pyYqbcn4y9_vav8gSg,96
36
- rcer_iot_client_pkg/services/epii/utils/update_thies_data_utils.py,sha256=husJv2E2FXROBvFU7k9jpYfFBH-tVj4Cma_zfajpjk4,414
37
- rcer_iot_client_pkg-0.3.1.dist-info/LICENSE,sha256=NWpf6b38xgBWPBo5HZsCbdfp9hZSliEbRqWQgm0fkOo,1076
38
- rcer_iot_client_pkg-0.3.1.dist-info/METADATA,sha256=85jl4vwLiuv_Nd8H4J5bcylq73IKZ1Uo4BQS8Vcl220,2582
39
- rcer_iot_client_pkg-0.3.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
40
- rcer_iot_client_pkg-0.3.1.dist-info/RECORD,,
38
+ rcer_iot_client_pkg/services/epii/use_cases/types/update_thies_data_types.py,sha256=JGmBwQtQB-nfopNlGwuI8Jf3A2QBFTGtrzjrDZDWMQ0,334
39
+ rcer_iot_client_pkg/services/epii/use_cases/update_thies_data.py,sha256=7H7Y6jDRb8ymsZv8spJ_NUWPBTiGFeD42FH1-eo1FS8,5268
40
+ rcer_iot_client_pkg/services/epii/utils/__init__.py,sha256=cYt2tvq65_OMjFaqb8-CCC7IGCQgFd4ziEUWJV7s1iY,98
41
+ rcer_iot_client_pkg/services/epii/utils/update_thies_data_utils.py,sha256=-q8t-xZmpwFDADGlDu0S7EKvZK7R0GUR6JqOxQShWgE,415
42
+ rcer_iot_client_pkg-0.4.0.dist-info/LICENSE,sha256=NWpf6b38xgBWPBo5HZsCbdfp9hZSliEbRqWQgm0fkOo,1076
43
+ rcer_iot_client_pkg-0.4.0.dist-info/METADATA,sha256=Byx8wk_eQeCPMTS_46IeXPkB6hxSlNW_M3Qhj-8x5Lo,3483
44
+ rcer_iot_client_pkg-0.4.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
45
+ rcer_iot_client_pkg-0.4.0.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- PATH_AVG_FILES = "ftp/thies/BINFILES/ARCH_AV1"
2
- PATH_EXT_FILES = "ftp/thies/BINFILES/ARCH_EX1"
3
- DRIVE_ID = "b!Row14jaFrU-1q8qzrvj3OmPPTYWXizFEpJmI-wsfH5pXxA0qQwgQS50m2xvPCZem"
4
- FILE_TYPES = ["AVG", "EXT"]