saviialib 0.6.2__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {saviialib-0.6.2 → saviialib-0.8.0}/PKG-INFO +23 -24
- {saviialib-0.6.2 → saviialib-0.8.0}/README.md +21 -23
- {saviialib-0.6.2 → saviialib-0.8.0}/pyproject.toml +2 -1
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/__init__.py +2 -2
- saviialib-0.8.0/src/saviialib/general_types/api/__init__.py +3 -0
- saviialib-0.8.0/src/saviialib/general_types/api/epii_api_types.py +81 -0
- saviialib-0.6.2/src/saviialib/general_types/error_types/api/update_thies_data_error_types.py → saviialib-0.8.0/src/saviialib/general_types/error_types/api/epii_api_error_types.py +36 -0
- saviialib-0.8.0/src/saviialib/libs/files_client/__init__.py +4 -0
- saviialib-0.8.0/src/saviialib/libs/files_client/clients/aiofiles_client.py +21 -0
- saviialib-0.8.0/src/saviialib/libs/files_client/files_client.py +28 -0
- saviialib-0.8.0/src/saviialib/libs/files_client/files_client_contract.py +13 -0
- saviialib-0.8.0/src/saviialib/libs/files_client/types/files_client_types.py +31 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/zero_dependency/utils/datetime_utils.py +2 -2
- saviialib-0.8.0/src/saviialib/services/epii/api.py +79 -0
- saviialib-0.8.0/src/saviialib/services/epii/controllers/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/controllers/types/update_thies_data_types.py +1 -1
- saviialib-0.8.0/src/saviialib/services/epii/controllers/types/upload_backup_to_sharepoint_types.py +16 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/controllers/update_thies_data.py +1 -1
- saviialib-0.8.0/src/saviialib/services/epii/controllers/upload_backup_to_sharepoint.py +85 -0
- saviialib-0.8.0/src/saviialib/services/epii/use_cases/constants/upload_backup_to_sharepoint_constants.py +5 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/use_cases/types/update_thies_data_types.py +1 -17
- saviialib-0.8.0/src/saviialib/services/epii/use_cases/types/upload_backup_to_sharepoint_types.py +8 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/use_cases/update_thies_data.py +5 -13
- saviialib-0.8.0/src/saviialib/services/epii/use_cases/upload_backup_to_sharepoint.py +201 -0
- saviialib-0.8.0/src/saviialib/services/epii/utils/upload_backup_to_sharepoint_utils.py +92 -0
- saviialib-0.6.2/src/saviialib/general_types/api/__init__.py +0 -3
- saviialib-0.6.2/src/saviialib/general_types/api/update_thies_data_types.py +0 -30
- saviialib-0.6.2/src/saviialib/services/epii/api.py +0 -26
- saviialib-0.6.2/src/saviialib/services/epii/controllers/__init__.py +0 -3
- {saviialib-0.6.2 → saviialib-0.8.0}/LICENSE +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/general_types/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/general_types/error_types/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/general_types/error_types/api/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/general_types/error_types/common/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/general_types/error_types/common/common_types.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/ftp_client/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/ftp_client/clients/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/ftp_client/clients/aioftp_client.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/ftp_client/ftp_client.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/ftp_client/ftp_client_contract.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/ftp_client/types/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/ftp_client/types/ftp_client_types.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/sharepoint_client/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/sharepoint_client/clients/sharepoint_rest_api.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/sharepoint_client/sharepoint_client.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/sharepoint_client/sharepoint_client_contract.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/sharepoint_client/types/sharepoint_client_types.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/controllers/types/__init__.py +0 -0
- {saviialib-0.6.2/src/saviialib/services/epii → saviialib-0.8.0/src/saviialib/services/epii/use_cases}/constants/update_thies_data_constants.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/use_cases/types/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/utils/__init__.py +0 -0
- {saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/utils/update_thies_data_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: saviialib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A client library for IoT projects in the RCER initiative
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: pedropablozavalat
|
|
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: aiofiles (==24.1.0)
|
|
14
15
|
Requires-Dist: aioftp (==0.25.1)
|
|
15
16
|
Requires-Dist: aiohttp (==3.11.16)
|
|
16
17
|
Requires-Dist: build
|
|
@@ -34,23 +35,11 @@ pip install saviialib
|
|
|
34
35
|
## Usage
|
|
35
36
|
|
|
36
37
|
### Initialize the EPii API Client
|
|
37
|
-
To start using the library, you need to create an `EpiiAPI` client instance:
|
|
38
|
+
To start using the library, you need to create an `EpiiAPI` client instance with its configuration class:
|
|
38
39
|
|
|
39
40
|
```python
|
|
40
|
-
from saviialib import EpiiAPI
|
|
41
|
-
|
|
42
|
-
api_client = EpiiAPI()
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Update THIES Data Logger Files
|
|
46
|
-
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:
|
|
47
|
-
|
|
48
|
-
```python
|
|
49
|
-
from saviialib import EpiiUpdateThiesConfig
|
|
50
|
-
import asyncio
|
|
51
|
-
|
|
52
|
-
async def update_thies_data():
|
|
53
|
-
config = EpiiUpdateThiesConfig(
|
|
41
|
+
from saviialib import EpiiAPI, EpiiAPIConfig
|
|
42
|
+
config = EpiiAPIConfig(
|
|
54
43
|
ftp_port=FTP_PORT,
|
|
55
44
|
ftp_host=FTP_HOST,
|
|
56
45
|
ftp_user=FTP_USER,
|
|
@@ -61,15 +50,26 @@ async def update_thies_data():
|
|
|
61
50
|
sharepoint_tenant_name=SHAREPOINT_TENANT_NAME,
|
|
62
51
|
sharepoint_site_name=SHAREPOINT_SITE_NAME
|
|
63
52
|
)
|
|
64
|
-
|
|
65
|
-
return response
|
|
66
|
-
|
|
67
|
-
asyncio.run(update_thies_data())
|
|
53
|
+
api_client = EpiiAPI(config)
|
|
68
54
|
```
|
|
69
|
-
|
|
70
55
|
**Notes:**
|
|
71
56
|
- 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.
|
|
72
57
|
|
|
58
|
+
### Update THIES Data Logger Files
|
|
59
|
+
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:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from saviialib import EpiiUpdateThiesConfig
|
|
63
|
+
import asyncio
|
|
64
|
+
|
|
65
|
+
async def main():
|
|
66
|
+
# Before calling this method, you must have initialised the api class ...
|
|
67
|
+
response = await api_client.update_thies_data()
|
|
68
|
+
return response
|
|
69
|
+
|
|
70
|
+
asyncio.run(main())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
73
|
## Development
|
|
74
74
|
|
|
75
75
|
This project includes a `Makefile` to simplify common tasks. Below are the available commands:
|
|
@@ -111,9 +111,8 @@ make lint
|
|
|
111
111
|
This command checks the codebase for linting issues and outputs any problems that need to be addressed.
|
|
112
112
|
|
|
113
113
|
## Contributing
|
|
114
|
-
If you're interested in contributing to this project, please follow the contributing guidelines.
|
|
115
|
-
|
|
116
|
-
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.
|
|
114
|
+
If you're interested in contributing to this project, please follow the contributing guidelines. By contributing to this project, you agree to abide by its terms.
|
|
115
|
+
Contributions are welcome and appreciated!
|
|
117
116
|
|
|
118
117
|
## License
|
|
119
118
|
|
|
@@ -14,23 +14,11 @@ pip install saviialib
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
16
16
|
### Initialize the EPii API Client
|
|
17
|
-
To start using the library, you need to create an `EpiiAPI` client instance:
|
|
17
|
+
To start using the library, you need to create an `EpiiAPI` client instance with its configuration class:
|
|
18
18
|
|
|
19
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(
|
|
20
|
+
from saviialib import EpiiAPI, EpiiAPIConfig
|
|
21
|
+
config = EpiiAPIConfig(
|
|
34
22
|
ftp_port=FTP_PORT,
|
|
35
23
|
ftp_host=FTP_HOST,
|
|
36
24
|
ftp_user=FTP_USER,
|
|
@@ -41,15 +29,26 @@ async def update_thies_data():
|
|
|
41
29
|
sharepoint_tenant_name=SHAREPOINT_TENANT_NAME,
|
|
42
30
|
sharepoint_site_name=SHAREPOINT_SITE_NAME
|
|
43
31
|
)
|
|
44
|
-
|
|
45
|
-
return response
|
|
46
|
-
|
|
47
|
-
asyncio.run(update_thies_data())
|
|
32
|
+
api_client = EpiiAPI(config)
|
|
48
33
|
```
|
|
49
|
-
|
|
50
34
|
**Notes:**
|
|
51
35
|
- 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
36
|
|
|
37
|
+
### Update THIES Data Logger Files
|
|
38
|
+
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:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from saviialib import EpiiUpdateThiesConfig
|
|
42
|
+
import asyncio
|
|
43
|
+
|
|
44
|
+
async def main():
|
|
45
|
+
# Before calling this method, you must have initialised the api class ...
|
|
46
|
+
response = await api_client.update_thies_data()
|
|
47
|
+
return response
|
|
48
|
+
|
|
49
|
+
asyncio.run(main())
|
|
50
|
+
```
|
|
51
|
+
|
|
53
52
|
## Development
|
|
54
53
|
|
|
55
54
|
This project includes a `Makefile` to simplify common tasks. Below are the available commands:
|
|
@@ -91,9 +90,8 @@ make lint
|
|
|
91
90
|
This command checks the codebase for linting issues and outputs any problems that need to be addressed.
|
|
92
91
|
|
|
93
92
|
## Contributing
|
|
94
|
-
If you're interested in contributing to this project, please follow the contributing guidelines.
|
|
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.
|
|
93
|
+
If you're interested in contributing to this project, please follow the contributing guidelines. By contributing to this project, you agree to abide by its terms.
|
|
94
|
+
Contributions are welcome and appreciated!
|
|
97
95
|
|
|
98
96
|
## License
|
|
99
97
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "saviialib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "A client library for IoT projects in the RCER initiative"
|
|
5
5
|
authors = ["pedropablozavalat"]
|
|
6
6
|
license = "MIT"
|
|
@@ -10,6 +10,7 @@ readme = "README.md"
|
|
|
10
10
|
python = "^3.10"
|
|
11
11
|
aioftp = "0.25.1"
|
|
12
12
|
aiohttp = "3.11.16"
|
|
13
|
+
aiofiles = "24.1.0"
|
|
13
14
|
dotenv = "0.9.9"
|
|
14
15
|
pytest-cov="6.1.1"
|
|
15
16
|
build="*"
|
|
@@ -4,6 +4,6 @@ from importlib.metadata import version
|
|
|
4
4
|
__version__ = version("saviialib")
|
|
5
5
|
|
|
6
6
|
from .services.epii.api import EpiiAPI
|
|
7
|
-
from .general_types.api.
|
|
7
|
+
from .general_types.api.epii_api_types import EpiiAPIConfig
|
|
8
8
|
|
|
9
|
-
__all__ = ["EpiiAPI", "
|
|
9
|
+
__all__ = ["EpiiAPI", "EpiiAPIConfig"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class EpiiAPIConfig:
|
|
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
|
+
"""
|
|
20
|
+
|
|
21
|
+
ftp_host: str
|
|
22
|
+
ftp_port: int
|
|
23
|
+
ftp_user: str
|
|
24
|
+
ftp_password: str
|
|
25
|
+
sharepoint_client_id: str
|
|
26
|
+
sharepoint_client_secret: str
|
|
27
|
+
sharepoint_tenant_id: str
|
|
28
|
+
sharepoint_tenant_name: str
|
|
29
|
+
sharepoint_site_name: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class FtpClientConfig:
|
|
34
|
+
ftp_host: str
|
|
35
|
+
ftp_port: int
|
|
36
|
+
ftp_user: str
|
|
37
|
+
ftp_password: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SharepointConfig:
|
|
42
|
+
sharepoint_client_id: str
|
|
43
|
+
sharepoint_client_secret: str
|
|
44
|
+
sharepoint_tenant_id: str
|
|
45
|
+
sharepoint_tenant_name: str
|
|
46
|
+
sharepoint_site_name: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class EpiiUpdateThiesConfig:
|
|
51
|
+
ftp_port: int
|
|
52
|
+
ftp_host: str
|
|
53
|
+
ftp_user: str
|
|
54
|
+
ftp_password: str
|
|
55
|
+
sharepoint_client_id: str
|
|
56
|
+
sharepoint_client_secret: str
|
|
57
|
+
sharepoint_tenant_id: str
|
|
58
|
+
sharepoint_tenant_name: str
|
|
59
|
+
sharepoint_site_name: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class EpiiSharepointBackupConfig:
|
|
64
|
+
"""
|
|
65
|
+
Configuration for backing up files to SharePoint.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
sharepoint_client_id (str): Client ID for SharePoint authentication.
|
|
69
|
+
sharepoint_client_secret (str): Client secret for SharePoint authentication.
|
|
70
|
+
sharepoint_tenant_id (str): Tenant ID for SharePoint authentication.
|
|
71
|
+
sharepoint_tenant_name (str): Tenant name for SharePoint.
|
|
72
|
+
sharepoint_site_name (str): Site name in SharePoint.
|
|
73
|
+
local_backup_source_path (str): Local path to backup.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
sharepoint_client_id: str
|
|
77
|
+
sharepoint_client_secret: str
|
|
78
|
+
sharepoint_tenant_id: str
|
|
79
|
+
sharepoint_tenant_name: str
|
|
80
|
+
sharepoint_site_name: str
|
|
81
|
+
local_backup_source_path: str
|
|
@@ -55,3 +55,39 @@ class SharePointUploadError(Exception):
|
|
|
55
55
|
"An error occurred while uploading files to the Microsoft SharePoint folder. "
|
|
56
56
|
+ self.reason.__str__()
|
|
57
57
|
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BackupUploadError(Exception):
|
|
61
|
+
"""Raised when there is an error when occurs the migration from the local backup to
|
|
62
|
+
sharepoint cloud."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, *args, reason):
|
|
65
|
+
super().__init__(*args, reason)
|
|
66
|
+
self.reason = reason
|
|
67
|
+
|
|
68
|
+
def __str__(self):
|
|
69
|
+
return (
|
|
70
|
+
"An error occurred during the migration from the local backup to SharePoint cloud. "
|
|
71
|
+
"Search the logs for more information. " + self.reason.__str__()
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BackupSourcePathError(Exception):
|
|
76
|
+
"""Raised when the local backup source path is invalid."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, *args, reason):
|
|
79
|
+
super().__init__(*args, reason)
|
|
80
|
+
self.reason = reason
|
|
81
|
+
|
|
82
|
+
def __str__(self):
|
|
83
|
+
return "Invalid local backup source path. " + self.reason.__str__()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class BackupEmptyError(Exception):
|
|
87
|
+
"""Raised when the local backup folder is empty."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, *args):
|
|
90
|
+
super().__init__(*args)
|
|
91
|
+
|
|
92
|
+
def __str__(self):
|
|
93
|
+
return "The local backup folder is empty. "
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import aiofiles
|
|
2
|
+
|
|
3
|
+
from saviialib.libs.files_client.files_client_contract import FilesClientContract
|
|
4
|
+
from saviialib.libs.files_client.types.files_client_types import (
|
|
5
|
+
FilesClientInitArgs,
|
|
6
|
+
ReadArgs,
|
|
7
|
+
WriteArgs,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AioFilesClient(FilesClientContract):
|
|
12
|
+
def __init__(self, args: FilesClientInitArgs):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
async def read(self, args: ReadArgs) -> str | bytes:
|
|
16
|
+
encoding = None if args.mode == "rb" else args.encoding
|
|
17
|
+
async with aiofiles.open(args.file_path, args.mode, encoding=encoding) as file:
|
|
18
|
+
return await file.read()
|
|
19
|
+
|
|
20
|
+
async def write(self, args: WriteArgs) -> None:
|
|
21
|
+
return None
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .clients.aiofiles_client import AioFilesClient
|
|
2
|
+
from .files_client_contract import FilesClientContract
|
|
3
|
+
from .types.files_client_types import FilesClientInitArgs, ReadArgs, WriteArgs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FilesClient(FilesClientContract):
|
|
7
|
+
CLIENTS = {"aiofiles_client"}
|
|
8
|
+
|
|
9
|
+
def __init__(self, args: FilesClientInitArgs) -> None:
|
|
10
|
+
if args.client_name not in FilesClient.CLIENTS:
|
|
11
|
+
msg = f"Unsupported client {args.client_name}"
|
|
12
|
+
raise KeyError(msg)
|
|
13
|
+
|
|
14
|
+
if args.client_name == "aiofiles_client":
|
|
15
|
+
self.client_obj = AioFilesClient(args)
|
|
16
|
+
self.client_name = args.client_name
|
|
17
|
+
|
|
18
|
+
async def read(self, args: ReadArgs):
|
|
19
|
+
"""Reads data from a specified source using the provided arguments.
|
|
20
|
+
|
|
21
|
+
:param args (ReadArgs): An object containing the parameters required for the read operation.
|
|
22
|
+
|
|
23
|
+
:return file: The result of the read operation, as returned by the client object.
|
|
24
|
+
"""
|
|
25
|
+
return await self.client_obj.read(args)
|
|
26
|
+
|
|
27
|
+
async def write(self, args: WriteArgs):
|
|
28
|
+
return await self.client_obj.write(args)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from .types.files_client_types import ReadArgs, WriteArgs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FilesClientContract(ABC):
|
|
7
|
+
@abstractmethod
|
|
8
|
+
async def read(self, args: ReadArgs) -> str | bytes:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
async def write(self, arg: WriteArgs) -> None:
|
|
13
|
+
pass
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class FilesClientInitArgs:
|
|
7
|
+
client_name: str = "aiofiles_client"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ReadArgs:
|
|
12
|
+
"""
|
|
13
|
+
Represents the arguments required to read a file.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
file_path (str): The path to the file to be read.
|
|
17
|
+
mode (Literal['r', 'rb']): The mode in which the file should be opened.
|
|
18
|
+
- 'r': Open for reading (text mode).
|
|
19
|
+
- 'rb': Open for reading (binary mode).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
file_path: str
|
|
23
|
+
mode: Literal["r", "rb"]
|
|
24
|
+
encoding: str = "utf-8"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class WriteArgs:
|
|
29
|
+
destination_path: str
|
|
30
|
+
file_name: str
|
|
31
|
+
file_content: str | bytes
|
{saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/zero_dependency/utils/datetime_utils.py
RENAMED
|
@@ -13,13 +13,13 @@ def today(timezone: str = "America/Santiago") -> str:
|
|
|
13
13
|
return datetime.now(tz=ZoneInfo(timezone))
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def datetime_to_str(date: datetime, date_format: str = "%Y
|
|
16
|
+
def datetime_to_str(date: datetime, date_format: str = "%m/%d/%Y, %H:%M:%S") -> str:
|
|
17
17
|
"""
|
|
18
18
|
Convert a datetime object to a string in the specified format.
|
|
19
19
|
|
|
20
20
|
:param date: The datetime object to convert.
|
|
21
21
|
:param date_format: The format to convert the datetime object to.
|
|
22
|
-
Defaults to "
|
|
22
|
+
Defaults to "%Y-%m-%dT%H:%M:%S".
|
|
23
23
|
:return: A string in the specified format.
|
|
24
24
|
"""
|
|
25
25
|
return date.strftime(date_format)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
from .controllers.types.update_thies_data_types import UpdateThiesDataControllerInput
|
|
4
|
+
from .controllers.types.upload_backup_to_sharepoint_types import (
|
|
5
|
+
UploadBackupToSharepointControllerInput,
|
|
6
|
+
)
|
|
7
|
+
from .controllers.update_thies_data import UpdateThiesDataController
|
|
8
|
+
from .controllers.upload_backup_to_sharepoint import UploadBackupToSharepointController
|
|
9
|
+
from saviialib.general_types.api.epii_api_types import (
|
|
10
|
+
EpiiUpdateThiesConfig,
|
|
11
|
+
EpiiSharepointBackupConfig,
|
|
12
|
+
EpiiAPIConfig,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EpiiAPI:
|
|
17
|
+
"""
|
|
18
|
+
EpiiAPI is a service class that provides methods to interact with Patagonia Center system.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config: EpiiAPIConfig):
|
|
22
|
+
self.ftp_port = config.ftp_port
|
|
23
|
+
self.ftp_host = config.ftp_host
|
|
24
|
+
self.ftp_user = config.ftp_user
|
|
25
|
+
self.ftp_password = config.ftp_password
|
|
26
|
+
self.sharepoint_client_id = config.sharepoint_client_id
|
|
27
|
+
self.sharepoint_client_secret = config.sharepoint_client_secret
|
|
28
|
+
self.sharepoint_tenant_id = config.sharepoint_tenant_id
|
|
29
|
+
self.sharepoint_tenant_name = config.sharepoint_tenant_name
|
|
30
|
+
self.sharepoint_site_name = config.sharepoint_site_name
|
|
31
|
+
|
|
32
|
+
async def update_thies_data(self) -> Dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
This method establishes a connection to an FTP server using the provided
|
|
35
|
+
credentials and updates data related to THIES Data Logger.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
response (dict): A dictionary representation of the API response.
|
|
39
|
+
"""
|
|
40
|
+
config = EpiiUpdateThiesConfig(
|
|
41
|
+
ftp_port=self.ftp_port,
|
|
42
|
+
ftp_host=self.ftp_host,
|
|
43
|
+
ftp_user=self.ftp_user,
|
|
44
|
+
ftp_password=self.ftp_password,
|
|
45
|
+
sharepoint_client_id=self.sharepoint_client_id,
|
|
46
|
+
sharepoint_client_secret=self.sharepoint_client_secret,
|
|
47
|
+
sharepoint_site_name=self.sharepoint_site_name,
|
|
48
|
+
sharepoint_tenant_id=self.sharepoint_tenant_id,
|
|
49
|
+
sharepoint_tenant_name=self.sharepoint_tenant_name,
|
|
50
|
+
)
|
|
51
|
+
controller = UpdateThiesDataController(UpdateThiesDataControllerInput(config))
|
|
52
|
+
response = await controller.execute()
|
|
53
|
+
return response.__dict__
|
|
54
|
+
|
|
55
|
+
async def upload_backup_to_sharepoint(
|
|
56
|
+
self, local_backup_source_path: str
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
"""Migrate a backup folder from Home assistant to Sharepoint directory.
|
|
59
|
+
Args:
|
|
60
|
+
local_backup_source_path (str): Local path to backup.
|
|
61
|
+
Returns:
|
|
62
|
+
response (dict): A dictionary containing the response from the upload operation.
|
|
63
|
+
This dictionary will typically include information about the success or
|
|
64
|
+
failure of the upload, as well as any relevant metadata.
|
|
65
|
+
"""
|
|
66
|
+
config = EpiiSharepointBackupConfig(
|
|
67
|
+
sharepoint_client_id=self.sharepoint_client_id,
|
|
68
|
+
sharepoint_client_secret=self.sharepoint_client_secret,
|
|
69
|
+
sharepoint_site_name=self.sharepoint_site_name,
|
|
70
|
+
sharepoint_tenant_id=self.sharepoint_tenant_id,
|
|
71
|
+
sharepoint_tenant_name=self.sharepoint_tenant_name,
|
|
72
|
+
local_backup_source_path=local_backup_source_path,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
controller = UploadBackupToSharepointController(
|
|
76
|
+
UploadBackupToSharepointControllerInput(config)
|
|
77
|
+
)
|
|
78
|
+
response = await controller.execute()
|
|
79
|
+
return response.__dict__
|
|
File without changes
|
saviialib-0.8.0/src/saviialib/services/epii/controllers/types/upload_backup_to_sharepoint_types.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from saviialib.general_types.api.epii_api_types import EpiiSharepointBackupConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class UploadBackupToSharepointControllerInput:
|
|
9
|
+
config: EpiiSharepointBackupConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class UploadBackupToSharepointControllerOutput:
|
|
14
|
+
message: str
|
|
15
|
+
status: int
|
|
16
|
+
metadata: Dict[str, str] = field(default_factory=dict)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from saviialib.general_types.api.epii_api_types import SharepointConfig
|
|
3
|
+
|
|
4
|
+
from saviialib.general_types.error_types.api.epii_api_error_types import (
|
|
5
|
+
BackupUploadError,
|
|
6
|
+
BackupSourcePathError,
|
|
7
|
+
BackupEmptyError,
|
|
8
|
+
)
|
|
9
|
+
from saviialib.general_types.error_types.common.common_types import (
|
|
10
|
+
EmptyDataError,
|
|
11
|
+
SharepointClientError,
|
|
12
|
+
)
|
|
13
|
+
from saviialib.services.epii.controllers.types.upload_backup_to_sharepoint_types import (
|
|
14
|
+
UploadBackupToSharepointControllerInput,
|
|
15
|
+
UploadBackupToSharepointControllerOutput,
|
|
16
|
+
)
|
|
17
|
+
from saviialib.services.epii.use_cases.types.upload_backup_to_sharepoint_types import (
|
|
18
|
+
UploadBackupToSharepointUseCaseInput,
|
|
19
|
+
)
|
|
20
|
+
from saviialib.services.epii.use_cases.upload_backup_to_sharepoint import (
|
|
21
|
+
UploadBackupToSharepointUsecase,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UploadBackupToSharepointController:
|
|
26
|
+
def __init__(self, input: UploadBackupToSharepointControllerInput):
|
|
27
|
+
self.use_case = UploadBackupToSharepointUsecase(
|
|
28
|
+
UploadBackupToSharepointUseCaseInput(
|
|
29
|
+
sharepoint_config=SharepointConfig(
|
|
30
|
+
sharepoint_client_id=input.config.sharepoint_client_id,
|
|
31
|
+
sharepoint_client_secret=input.config.sharepoint_client_secret,
|
|
32
|
+
sharepoint_site_name=input.config.sharepoint_site_name,
|
|
33
|
+
sharepoint_tenant_name=input.config.sharepoint_tenant_name,
|
|
34
|
+
sharepoint_tenant_id=input.config.sharepoint_tenant_id,
|
|
35
|
+
),
|
|
36
|
+
local_backup_source_path=input.config.local_backup_source_path,
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async def execute(self) -> UploadBackupToSharepointControllerOutput:
|
|
41
|
+
try:
|
|
42
|
+
data = await self.use_case.execute()
|
|
43
|
+
return UploadBackupToSharepointControllerOutput(
|
|
44
|
+
message="Local backup was migrated successfully",
|
|
45
|
+
status=HTTPStatus.OK.value,
|
|
46
|
+
metadata={"data": data},
|
|
47
|
+
)
|
|
48
|
+
except EmptyDataError:
|
|
49
|
+
return UploadBackupToSharepointControllerOutput(
|
|
50
|
+
message="No files to upload", status=HTTPStatus.NO_CONTENT.value
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
except (AttributeError, NameError, ValueError) as error:
|
|
54
|
+
return UploadBackupToSharepointControllerOutput(
|
|
55
|
+
message="An unexpected error occurred during use case initialization.",
|
|
56
|
+
status=HTTPStatus.BAD_REQUEST.value,
|
|
57
|
+
metadata={"error": error.__str__()},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
except SharepointClientError as error:
|
|
61
|
+
return UploadBackupToSharepointControllerOutput(
|
|
62
|
+
message="Sharepoint Client initialization fails.",
|
|
63
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR.value,
|
|
64
|
+
metadata={"error": error.__str__()},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
except BackupUploadError as error:
|
|
68
|
+
return UploadBackupToSharepointControllerOutput(
|
|
69
|
+
message="An error occurred during local backup",
|
|
70
|
+
status=HTTPStatus.MULTI_STATUS.value,
|
|
71
|
+
metadata={"error": error.__str__()},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
except BackupSourcePathError as error:
|
|
75
|
+
return UploadBackupToSharepointControllerOutput(
|
|
76
|
+
message="Invalid local backup source path provided.",
|
|
77
|
+
status=HTTPStatus.BAD_REQUEST.value,
|
|
78
|
+
metadata={"error": error.__str__()},
|
|
79
|
+
)
|
|
80
|
+
except BackupEmptyError as error:
|
|
81
|
+
return UploadBackupToSharepointControllerOutput(
|
|
82
|
+
message="Each folder in the backup folder is empty. Check out again",
|
|
83
|
+
status=HTTPStatus.EXPECTATION_FAILED.value,
|
|
84
|
+
metadata={"error": error.__str__()},
|
|
85
|
+
)
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
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
|
|
3
|
+
from saviialib.general_types.api.epii_api_types import FtpClientConfig, SharepointConfig
|
|
20
4
|
|
|
21
5
|
|
|
22
6
|
@dataclass
|
{saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/use_cases/update_thies_data.py
RENAMED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
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 (
|
|
1
|
+
import saviialib.services.epii.use_cases.constants.update_thies_data_constants as c
|
|
2
|
+
from saviialib.general_types.error_types.api.epii_api_error_types import (
|
|
5
3
|
SharePointFetchingError,
|
|
6
4
|
SharePointUploadError,
|
|
7
5
|
ThiesConnectionError,
|
|
@@ -33,8 +31,6 @@ from saviialib.services.epii.utils import (
|
|
|
33
31
|
parse_execute_response,
|
|
34
32
|
)
|
|
35
33
|
|
|
36
|
-
load_dotenv()
|
|
37
|
-
|
|
38
34
|
|
|
39
35
|
class UpdateThiesDataUseCase:
|
|
40
36
|
def __init__(self, input: UpdateThiesDataUseCaseInput):
|
|
@@ -122,7 +118,7 @@ class UpdateThiesDataUseCase:
|
|
|
122
118
|
self, files: dict
|
|
123
119
|
) -> dict[str, list[str]]:
|
|
124
120
|
"""Upload files to SharePoint and categorize the results."""
|
|
125
|
-
upload_results = {"failed_files": [], "
|
|
121
|
+
upload_results = {"failed_files": [], "new_files": []}
|
|
126
122
|
|
|
127
123
|
async with self.sharepoint_client:
|
|
128
124
|
for file, file_content in files.items():
|
|
@@ -133,12 +129,8 @@ class UpdateThiesDataUseCase:
|
|
|
133
129
|
file_content=file_content,
|
|
134
130
|
file_name=file_name,
|
|
135
131
|
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if response.get("Exists", False):
|
|
139
|
-
upload_results["overwritten_files"].append(file)
|
|
140
|
-
else:
|
|
141
|
-
upload_results["new_files"].append(file)
|
|
132
|
+
await self.sharepoint_client.upload_file(args)
|
|
133
|
+
upload_results["new_files"].append(file)
|
|
142
134
|
|
|
143
135
|
except ConnectionError as error:
|
|
144
136
|
upload_results["failed_files"].append(
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from time import time
|
|
4
|
+
import saviialib.services.epii.use_cases.constants.upload_backup_to_sharepoint_constants as c
|
|
5
|
+
from saviialib.general_types.error_types.api.epii_api_error_types import (
|
|
6
|
+
BackupEmptyError,
|
|
7
|
+
BackupSourcePathError,
|
|
8
|
+
BackupUploadError,
|
|
9
|
+
)
|
|
10
|
+
from saviialib.general_types.error_types.common import (
|
|
11
|
+
SharepointClientError,
|
|
12
|
+
)
|
|
13
|
+
from saviialib.libs.files_client import FilesClient, FilesClientInitArgs, ReadArgs
|
|
14
|
+
from saviialib.libs.sharepoint_client import (
|
|
15
|
+
SharepointClient,
|
|
16
|
+
SharepointClientInitArgs,
|
|
17
|
+
SpUploadFileArgs,
|
|
18
|
+
)
|
|
19
|
+
from saviialib.services.epii.utils.upload_backup_to_sharepoint_utils import (
|
|
20
|
+
calculate_percentage_uploaded,
|
|
21
|
+
count_files_in_directory,
|
|
22
|
+
directory_exists,
|
|
23
|
+
extract_error_message,
|
|
24
|
+
parse_execute_response,
|
|
25
|
+
show_upload_result,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from .types.upload_backup_to_sharepoint_types import (
|
|
29
|
+
UploadBackupToSharepointUseCaseInput,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UploadBackupToSharepointUsecase:
|
|
34
|
+
def __init__(self, input: UploadBackupToSharepointUseCaseInput):
|
|
35
|
+
self.sharepoint_config = input.sharepoint_config
|
|
36
|
+
self.local_backup_source_path = input.local_backup_source_path
|
|
37
|
+
self.grouped_files_by_folder = self._extract_filesnames_by_folder()
|
|
38
|
+
self.files_client = self._initialize_files_client()
|
|
39
|
+
self.total_files = sum(
|
|
40
|
+
len(files) for files in self.grouped_files_by_folder.values()
|
|
41
|
+
)
|
|
42
|
+
self.log_history = []
|
|
43
|
+
|
|
44
|
+
def _initialize_files_client(self):
|
|
45
|
+
return FilesClient(FilesClientInitArgs(client_name="aiofiles_client"))
|
|
46
|
+
|
|
47
|
+
def _extract_filesnames_by_folder(self) -> dict[str, list[str]]:
|
|
48
|
+
"""Groups files by their parent folder."""
|
|
49
|
+
if not os.path.exists(self.local_backup_source_path):
|
|
50
|
+
return {}
|
|
51
|
+
return {
|
|
52
|
+
folder_name: [
|
|
53
|
+
file_name
|
|
54
|
+
for file_name in os.listdir(
|
|
55
|
+
os.path.join(self.local_backup_source_path, folder_name)
|
|
56
|
+
)
|
|
57
|
+
]
|
|
58
|
+
for folder_name in os.listdir(self.local_backup_source_path)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def _save_log_history(self) -> None:
|
|
62
|
+
text_file = "\n".join(self.log_history)
|
|
63
|
+
log_history_filepath = "BACKUP_LOG_HISTORY.log"
|
|
64
|
+
with open(log_history_filepath, "w") as file:
|
|
65
|
+
file.write(text_file)
|
|
66
|
+
|
|
67
|
+
async def export_file_to_sharepoint(
|
|
68
|
+
self, folder_name: str, file_name: str, file_content: bytes
|
|
69
|
+
) -> tuple[bool, str]:
|
|
70
|
+
"""Uploads a file to the specified folder in SharePoint."""
|
|
71
|
+
uploaded = None
|
|
72
|
+
error_message = ""
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
sharepoint_client = SharepointClient(
|
|
76
|
+
SharepointClientInitArgs(
|
|
77
|
+
self.sharepoint_config, client_name="sharepoint_rest_api"
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
except ConnectionError as error:
|
|
81
|
+
raise SharepointClientError(error)
|
|
82
|
+
|
|
83
|
+
async with sharepoint_client:
|
|
84
|
+
try:
|
|
85
|
+
destination_folder = c.DESTINATION_FOLDERS.get(folder_name, folder_name)
|
|
86
|
+
folder_url = f"{c.SHAREPOINT_BASE_URL}/{destination_folder}"
|
|
87
|
+
args = SpUploadFileArgs(
|
|
88
|
+
folder_relative_url=folder_url,
|
|
89
|
+
file_content=file_content,
|
|
90
|
+
file_name=file_name,
|
|
91
|
+
)
|
|
92
|
+
await sharepoint_client.upload_file(args)
|
|
93
|
+
uploaded = True
|
|
94
|
+
except ConnectionError as error:
|
|
95
|
+
error_message = str(error)
|
|
96
|
+
uploaded = False
|
|
97
|
+
|
|
98
|
+
return uploaded, error_message
|
|
99
|
+
|
|
100
|
+
async def upload_and_log_progress_task(self, folder_name, file_name) -> dict:
|
|
101
|
+
"""Task for uploads a file and logs progress."""
|
|
102
|
+
uploading_message = (
|
|
103
|
+
f"[BACKUP] Uploading file '{file_name}' from '{folder_name}' "
|
|
104
|
+
)
|
|
105
|
+
self.log_history.append(uploading_message)
|
|
106
|
+
print(uploading_message)
|
|
107
|
+
file_path = os.path.join(self.local_backup_source_path, folder_name, file_name)
|
|
108
|
+
file_content = await self.files_client.read(ReadArgs(file_path, mode="rb"))
|
|
109
|
+
uploaded, error_message = await self.export_file_to_sharepoint(
|
|
110
|
+
folder_name, file_name, file_content
|
|
111
|
+
)
|
|
112
|
+
result_message = show_upload_result(uploaded, file_name)
|
|
113
|
+
print(result_message)
|
|
114
|
+
self.log_history.append(result_message)
|
|
115
|
+
return {
|
|
116
|
+
"parent_folder": folder_name,
|
|
117
|
+
"file_name": file_name,
|
|
118
|
+
"uploaded": uploaded,
|
|
119
|
+
"error_message": error_message,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async def retry_upload_failed_files(self, results) -> None:
|
|
123
|
+
failed_files = [item for item in results if not item["uploaded"]]
|
|
124
|
+
tasks = []
|
|
125
|
+
retry_message = (
|
|
126
|
+
f"[BACKUP] Retrying upload for {len(failed_files)} failed files... 🚨"
|
|
127
|
+
)
|
|
128
|
+
self.log_history.append(retry_message)
|
|
129
|
+
print(retry_message)
|
|
130
|
+
for file in failed_files:
|
|
131
|
+
tasks.append(
|
|
132
|
+
self.upload_and_log_progress_task(
|
|
133
|
+
file["parent_folder"], file["file_name"]
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
137
|
+
success = calculate_percentage_uploaded(results, self.total_files)
|
|
138
|
+
if success < 100.0:
|
|
139
|
+
raise BackupUploadError(reason=extract_error_message(results, success))
|
|
140
|
+
else:
|
|
141
|
+
successful_upload_retry = (
|
|
142
|
+
"[BACKUP] All files uploaded successfully after retry."
|
|
143
|
+
)
|
|
144
|
+
self.log_history.append(successful_upload_retry)
|
|
145
|
+
print(successful_upload_retry)
|
|
146
|
+
self._save_log_history()
|
|
147
|
+
return parse_execute_response(results)
|
|
148
|
+
|
|
149
|
+
async def execute(self):
|
|
150
|
+
"""Exports all files from the local backup folder to SharePoint cloud."""
|
|
151
|
+
tasks = []
|
|
152
|
+
start_time = time()
|
|
153
|
+
if not directory_exists(self.local_backup_source_path):
|
|
154
|
+
raise BackupSourcePathError(
|
|
155
|
+
reason=f"'{self.local_backup_source_path}' doesn't exist."
|
|
156
|
+
)
|
|
157
|
+
if self.total_files == 0:
|
|
158
|
+
no_files_message = (
|
|
159
|
+
f"[BACKUP] {self.local_backup_source_path} has no files ⚠️"
|
|
160
|
+
)
|
|
161
|
+
self.log_history.append(no_files_message)
|
|
162
|
+
print(no_files_message)
|
|
163
|
+
raise BackupEmptyError
|
|
164
|
+
# Create task for each file stored in the the local backup folder.
|
|
165
|
+
for folder_name in self.grouped_files_by_folder:
|
|
166
|
+
if (
|
|
167
|
+
count_files_in_directory(self.local_backup_source_path, folder_name)
|
|
168
|
+
== 0
|
|
169
|
+
):
|
|
170
|
+
empty_folder_message = f"[BACKUP] The folder '{folder_name}' is empty ⚠️"
|
|
171
|
+
print(empty_folder_message)
|
|
172
|
+
self.log_history.append(empty_folder_message)
|
|
173
|
+
continue
|
|
174
|
+
extracting_files_message = (
|
|
175
|
+
"[BACKUP]" + f" Extracting files from '{folder_name} ".center(15, "*")
|
|
176
|
+
)
|
|
177
|
+
self.log_history.append(extracting_files_message)
|
|
178
|
+
print(extracting_files_message)
|
|
179
|
+
for file_name in self.grouped_files_by_folder[folder_name]:
|
|
180
|
+
tasks.append(self.upload_and_log_progress_task(folder_name, file_name))
|
|
181
|
+
|
|
182
|
+
# Execution of multiple asynchronous tasks for files migration.
|
|
183
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
184
|
+
success = calculate_percentage_uploaded(results, self.total_files)
|
|
185
|
+
if success < 100.0:
|
|
186
|
+
await self.retry_upload_failed_files(results)
|
|
187
|
+
else:
|
|
188
|
+
end_time = time()
|
|
189
|
+
backup_time = end_time - start_time
|
|
190
|
+
successful_backup_message = (
|
|
191
|
+
f"[BACKUP] Migration time: {backup_time:.2f} seconds ✨"
|
|
192
|
+
)
|
|
193
|
+
self.log_history.append(successful_backup_message)
|
|
194
|
+
|
|
195
|
+
finished_backup_message = (
|
|
196
|
+
"[BACKUP] All the files were uploaded successfully 🎉"
|
|
197
|
+
)
|
|
198
|
+
self.log_history.append(finished_backup_message)
|
|
199
|
+
|
|
200
|
+
self._save_log_history()
|
|
201
|
+
return parse_execute_response(results)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List, Dict, Optional
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def extract_error_information(error: str) -> Optional[Dict[str, str]]:
|
|
7
|
+
match = re.search(r"(\d+), message='([^']*)', url=\"([^\"]*)\"", error)
|
|
8
|
+
if match:
|
|
9
|
+
return {
|
|
10
|
+
"status_code": match.group(1),
|
|
11
|
+
"message": match.group(2),
|
|
12
|
+
"url": match.group(3),
|
|
13
|
+
}
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def explain_status_code(status_code: int) -> str:
|
|
18
|
+
explanations = {
|
|
19
|
+
404: "Probably an error with file or folder source path.",
|
|
20
|
+
403: "Permission denied when accessing the source path.",
|
|
21
|
+
500: "Internal server error occurred during upload.",
|
|
22
|
+
}
|
|
23
|
+
return explanations.get(status_code, "Unknown error occurred.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def extract_error_message(results: List[Dict], success: float) -> str:
|
|
27
|
+
print(
|
|
28
|
+
"[BACKUP] Not all files uploaded ⚠️\n"
|
|
29
|
+
f"[BACKUP] Files failed to upload: {(1 - success):.2%}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
failed_files = [item for item in results if not item.get("uploaded")]
|
|
33
|
+
|
|
34
|
+
error_data = []
|
|
35
|
+
for item in failed_files:
|
|
36
|
+
error_info = extract_error_information(item.get("error_message", ""))
|
|
37
|
+
if error_info:
|
|
38
|
+
error_data.append(
|
|
39
|
+
{
|
|
40
|
+
"file_name": item["file_name"],
|
|
41
|
+
"status_code": error_info["status_code"],
|
|
42
|
+
"message": error_info["message"],
|
|
43
|
+
"url": error_info["url"],
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Group errors by code.
|
|
48
|
+
grouped_errors: Dict[str, List[Dict]] = {}
|
|
49
|
+
for error in error_data:
|
|
50
|
+
code = error["status_code"]
|
|
51
|
+
grouped_errors.setdefault(code, []).append(error)
|
|
52
|
+
|
|
53
|
+
# Summary
|
|
54
|
+
for code, items in grouped_errors.items():
|
|
55
|
+
print(f"[BACKUP] Status code {code} - {explain_status_code(int(code))}")
|
|
56
|
+
for item in items:
|
|
57
|
+
print(
|
|
58
|
+
f"[BACKUP] File {item['file_name']}, url: {item['url']}, message: {item['message']}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
failed_file_names = [item["file_name"] for item in failed_files]
|
|
62
|
+
return f"Failed files: {', '.join(failed_file_names)}."
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_execute_response(results: List[Dict]) -> Dict[str, List[str]]:
|
|
66
|
+
return {
|
|
67
|
+
"new_files": len(
|
|
68
|
+
[item["file_name"] for item in results if item.get("uploaded")]
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def show_upload_result(uploaded: bool, file_name: str) -> str:
|
|
74
|
+
status = "✅" if uploaded else "❌"
|
|
75
|
+
message = "was uploaded successfully" if uploaded else "failed to upload"
|
|
76
|
+
result = f"[BACKUP] File {file_name} {message} {status}"
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def calculate_percentage_uploaded(results: List[Dict], total_files: int) -> float:
|
|
81
|
+
uploaded_count = sum(
|
|
82
|
+
1 for result in results if isinstance(result, dict) and result.get("uploaded")
|
|
83
|
+
)
|
|
84
|
+
return (uploaded_count / total_files) * 100 if total_files > 0 else 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def directory_exists(path: str) -> bool:
|
|
88
|
+
return os.path.exists(path)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def count_files_in_directory(path: str, folder_name: str) -> int:
|
|
92
|
+
return len(os.listdir(os.path.join(path, folder_name)))
|
|
@@ -1,30 +0,0 @@
|
|
|
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
|
|
@@ -1,26 +0,0 @@
|
|
|
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__
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/general_types/error_types/common/__init__.py
RENAMED
|
File without changes
|
{saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/general_types/error_types/common/common_types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/libs/sharepoint_client/sharepoint_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/controllers/types/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{saviialib-0.6.2 → saviialib-0.8.0}/src/saviialib/services/epii/utils/update_thies_data_utils.py
RENAMED
|
File without changes
|