saviialib 0.9.1__py3-none-any.whl → 1.6.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.

Potentially problematic release.


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

Files changed (96) hide show
  1. saviialib/__init__.py +73 -3
  2. saviialib/general_types/api/__init__.py +0 -3
  3. saviialib/general_types/api/{epii_api_types.py → saviia_api_types.py} +4 -38
  4. saviialib/general_types/api/saviia_backup_api_types.py +24 -0
  5. saviialib/general_types/api/saviia_netcamera_api_types.py +11 -0
  6. saviialib/general_types/api/saviia_shakes_api_types.py +21 -0
  7. saviialib/general_types/api/saviia_thies_api_types.py +31 -0
  8. saviialib/general_types/error_types/api/{epii_api_error_types.py → saviia_api_error_types.py} +20 -0
  9. saviialib/general_types/error_types/api/saviia_netcamera_error_types.py +7 -0
  10. saviialib/general_types/error_types/common/common_types.py +9 -0
  11. saviialib/libs/directory_client/__init__.py +4 -0
  12. saviialib/libs/directory_client/client/os_client.py +55 -0
  13. saviialib/libs/directory_client/directory_client.py +44 -0
  14. saviialib/libs/directory_client/directory_client_contract.py +40 -0
  15. saviialib/libs/directory_client/types/directory_client_types.py +6 -0
  16. saviialib/libs/ffmpeg_client/__init__.py +8 -0
  17. saviialib/libs/ffmpeg_client/clients/ffmpeg_asyncio_client.py +101 -0
  18. saviialib/libs/ffmpeg_client/ffmpeg_client.py +25 -0
  19. saviialib/libs/ffmpeg_client/ffmpeg_client_contract.py +12 -0
  20. saviialib/libs/ffmpeg_client/types/ffmpeg_client_types.py +28 -0
  21. saviialib/libs/files_client/__init__.py +2 -2
  22. saviialib/libs/files_client/clients/aiofiles_client.py +26 -3
  23. saviialib/libs/files_client/clients/csv_client.py +42 -0
  24. saviialib/libs/files_client/files_client.py +5 -7
  25. saviialib/libs/files_client/types/files_client_types.py +5 -4
  26. saviialib/libs/ftp_client/clients/aioftp_client.py +13 -6
  27. saviialib/libs/ftp_client/clients/ftplib_client.py +58 -0
  28. saviialib/libs/ftp_client/ftp_client.py +8 -5
  29. saviialib/libs/ftp_client/ftp_client_contract.py +2 -2
  30. saviialib/libs/log_client/__init__.py +19 -0
  31. saviialib/libs/log_client/log_client.py +46 -0
  32. saviialib/libs/log_client/log_client_contract.py +28 -0
  33. saviialib/libs/log_client/logging_client/logging_client.py +58 -0
  34. saviialib/libs/log_client/types/log_client_types.py +47 -0
  35. saviialib/libs/log_client/utils/log_client_utils.py +6 -0
  36. saviialib/libs/sftp_client/__init__.py +8 -0
  37. saviialib/libs/sftp_client/clients/asyncssh_sftp_client.py +83 -0
  38. saviialib/libs/sftp_client/sftp_client.py +26 -0
  39. saviialib/libs/sftp_client/sftp_client_contract.py +13 -0
  40. saviialib/libs/sftp_client/types/sftp_client_types.py +24 -0
  41. saviialib/libs/sharepoint_client/__init__.py +2 -0
  42. saviialib/libs/sharepoint_client/clients/sharepoint_rest_api.py +31 -6
  43. saviialib/libs/sharepoint_client/sharepoint_client.py +25 -1
  44. saviialib/libs/sharepoint_client/sharepoint_client_contract.py +5 -0
  45. saviialib/libs/sharepoint_client/types/sharepoint_client_types.py +5 -0
  46. saviialib/libs/zero_dependency/utils/booleans_utils.py +2 -0
  47. saviialib/libs/zero_dependency/utils/datetime_utils.py +1 -1
  48. saviialib/libs/zero_dependency/utils/strings_utils.py +5 -0
  49. saviialib/services/backup/api.py +36 -0
  50. saviialib/services/backup/controllers/__init__.py +0 -0
  51. saviialib/services/{epii → backup}/controllers/types/__init__.py +1 -1
  52. saviialib/services/{epii → backup}/controllers/types/upload_backup_to_sharepoint_types.py +4 -2
  53. saviialib/services/{epii → backup}/controllers/upload_backup_to_sharepoint.py +9 -8
  54. saviialib/services/backup/use_cases/constants/upload_backup_to_sharepoint_constants.py +5 -0
  55. saviialib/services/{epii → backup}/use_cases/types/__init__.py +1 -1
  56. saviialib/services/{epii → backup}/use_cases/types/upload_backup_to_sharepoint_types.py +4 -2
  57. saviialib/services/backup/use_cases/upload_backup_to_sharepoint.py +474 -0
  58. saviialib/services/backup/utils/__init__.py +3 -0
  59. saviialib/services/backup/utils/upload_backup_to_sharepoint_utils.py +100 -0
  60. saviialib/services/netcamera/api.py +30 -0
  61. saviialib/services/netcamera/controllers/get_media_files.py +40 -0
  62. saviialib/services/netcamera/controllers/types/get_media_files_types.py +16 -0
  63. saviialib/services/netcamera/use_cases/get_media_files.py +76 -0
  64. saviialib/services/netcamera/use_cases/types/get_media_files_types.py +18 -0
  65. saviialib/services/shakes/__init__.py +0 -0
  66. saviialib/services/shakes/api.py +31 -0
  67. saviialib/services/shakes/controllers/get_miniseed_files.py +48 -0
  68. saviialib/services/shakes/controllers/types/get_miniseed_files_types.py +16 -0
  69. saviialib/services/shakes/use_cases/get_miniseed_files.py +79 -0
  70. saviialib/services/shakes/use_cases/types/get_miniseed_files_types.py +18 -0
  71. saviialib/services/shakes/use_cases/utils/get_miniseed_files_utils.py +11 -0
  72. saviialib/services/thies/__init__.py +0 -0
  73. saviialib/services/thies/api.py +42 -0
  74. saviialib/services/thies/constants/update_thies_data_constants.py +67 -0
  75. saviialib/services/{epii → thies}/controllers/types/update_thies_data_types.py +5 -4
  76. saviialib/services/{epii → thies}/controllers/update_thies_data.py +18 -6
  77. saviialib/services/thies/use_cases/components/create_thies_statistics_file.py +115 -0
  78. saviialib/services/thies/use_cases/components/thies_bp.py +442 -0
  79. saviialib/services/{epii → thies}/use_cases/types/update_thies_data_types.py +10 -2
  80. saviialib/services/thies/use_cases/update_thies_data.py +391 -0
  81. saviialib-1.6.0.dist-info/METADATA +126 -0
  82. saviialib-1.6.0.dist-info/RECORD +96 -0
  83. {saviialib-0.9.1.dist-info → saviialib-1.6.0.dist-info}/WHEEL +1 -1
  84. saviialib/services/epii/api.py +0 -80
  85. saviialib/services/epii/use_cases/constants/update_thies_data_constants.py +0 -5
  86. saviialib/services/epii/use_cases/constants/upload_backup_to_sharepoint_constants.py +0 -5
  87. saviialib/services/epii/use_cases/update_thies_data.py +0 -171
  88. saviialib/services/epii/use_cases/upload_backup_to_sharepoint.py +0 -241
  89. saviialib/services/epii/utils/__init__.py +0 -3
  90. saviialib/services/epii/utils/upload_backup_to_sharepoint_utils.py +0 -102
  91. saviialib-0.9.1.dist-info/METADATA +0 -120
  92. saviialib-0.9.1.dist-info/RECORD +0 -49
  93. /saviialib/{services/epii → libs/log_client/types}/__init__.py +0 -0
  94. /saviialib/services/{epii/controllers → backup}/__init__.py +0 -0
  95. /saviialib/services/{epii → thies}/utils/update_thies_data_utils.py +0 -0
  96. {saviialib-0.9.1.dist-info → saviialib-1.6.0.dist-info/licenses}/LICENSE +0 -0
saviialib/__init__.py CHANGED
@@ -3,7 +3,77 @@ from importlib.metadata import version
3
3
 
4
4
  __version__ = version("saviialib")
5
5
 
6
- from .services.epii.api import EpiiAPI
7
- from .general_types.api.epii_api_types import EpiiAPIConfig
6
+ from .general_types.api.saviia_api_types import SaviiaAPIConfig
8
7
 
9
- __all__ = ["EpiiAPI", "EpiiAPIConfig"]
8
+ from typing import Dict, Type, Any, overload, Literal, List
9
+
10
+ from saviialib.services.backup.api import SaviiaBackupAPI
11
+ from saviialib.services.thies.api import SaviiaThiesAPI
12
+ from saviialib.general_types.api.saviia_thies_api_types import SaviiaThiesConfig
13
+ from saviialib.general_types.api.saviia_backup_api_types import SaviiaBackupConfig
14
+
15
+ __all__ = ["SaviiaAPI", "SaviiaAPIConfig"]
16
+
17
+
18
+ class SaviiaAPI:
19
+ API_REGISTRY: Dict[str, Type] = {
20
+ "thies": SaviiaThiesAPI,
21
+ "backup": SaviiaBackupAPI,
22
+ }
23
+
24
+ @overload
25
+ def get(self, name: Literal["thies"]) -> SaviiaThiesAPI: ...
26
+ @overload
27
+ def get(self, name: Literal["backup"]) -> SaviiaBackupAPI: ...
28
+
29
+ def __init__(self, config: SaviiaAPIConfig):
30
+ """
31
+ Receive a dictionary of configurations, with the same key
32
+ as those registered in API_REGISTRY.
33
+
34
+ :params configs: Dictionary of configurations for each API.
35
+
36
+ Example:
37
+ configs = {
38
+ "thies": SaviiaThiesConfig(...),
39
+ "backup": SaviiaBackupConfig(...)
40
+ }
41
+ """
42
+ self._instances: Dict[str, Any] = {}
43
+
44
+ for name, api_class in SaviiaAPI.API_REGISTRY.items():
45
+ if name == "thies":
46
+ service_config = SaviiaThiesConfig(
47
+ ftp_host=config.ftp_host,
48
+ ftp_port=config.ftp_port,
49
+ ftp_user=config.ftp_user,
50
+ ftp_password=config.ftp_password,
51
+ sharepoint_client_id=config.sharepoint_client_id,
52
+ sharepoint_client_secret=config.sharepoint_client_secret,
53
+ sharepoint_tenant_id=config.sharepoint_tenant_id,
54
+ sharepoint_tenant_name=config.sharepoint_tenant_name,
55
+ sharepoint_site_name=config.sharepoint_site_name,
56
+ logger=config.logger,
57
+ )
58
+ elif name == "backup":
59
+ service_config = SaviiaBackupConfig(
60
+ sharepoint_client_id=config.sharepoint_client_id,
61
+ sharepoint_client_secret=config.sharepoint_client_secret,
62
+ sharepoint_tenant_id=config.sharepoint_tenant_id,
63
+ sharepoint_tenant_name=config.sharepoint_tenant_name,
64
+ sharepoint_site_name=config.sharepoint_site_name,
65
+ logger=config.logger,
66
+ )
67
+
68
+ self._instances[name] = api_class(service_config)
69
+
70
+ def get(self, name: Literal["thies", "backup"]) -> Any:
71
+ """Returns the API instance associated with the given name."""
72
+ try:
73
+ return self._instances[name]
74
+ except KeyError:
75
+ raise ValueError(f"API '{name}' is not registered or not configured.")
76
+
77
+ def list_available(self) -> List[str]:
78
+ """List of available registered APIs."""
79
+ return list(self._instances.keys())
@@ -1,3 +0,0 @@
1
- from .epii_api_types import EpiiUpdateThiesConfig, EpiiSharepointBackupConfig
2
-
3
- __all__ = ["EpiiUpdateThiesConfig", "EpiiSharepointBackupConfig"]
@@ -1,10 +1,11 @@
1
1
  from dataclasses import dataclass
2
+ from logging import Logger
2
3
 
3
4
 
4
5
  @dataclass
5
- class EpiiAPIConfig:
6
+ class SaviiaAPIConfig:
6
7
  """
7
- Configuration for Epii API.
8
+ Configuration for SAVIIA API.
8
9
 
9
10
  Attributes:
10
11
  ftp_port (int): Port number of the FTP server.
@@ -27,6 +28,7 @@ class EpiiAPIConfig:
27
28
  sharepoint_tenant_id: str
28
29
  sharepoint_tenant_name: str
29
30
  sharepoint_site_name: str
31
+ logger: Logger
30
32
 
31
33
 
32
34
  @dataclass
@@ -44,39 +46,3 @@ class SharepointConfig:
44
46
  sharepoint_tenant_id: str
45
47
  sharepoint_tenant_name: str
46
48
  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
82
- destination_folders: str
@@ -0,0 +1,24 @@
1
+ from dataclasses import dataclass
2
+ from logging import Logger
3
+
4
+
5
+ @dataclass
6
+ class SaviiaBackupConfig:
7
+ """
8
+ Configuration for backing up files to SharePoint.
9
+
10
+ Attributes:
11
+ sharepoint_client_id (str): Client ID for SharePoint authentication.
12
+ sharepoint_client_secret (str): Client secret for SharePoint authentication.
13
+ sharepoint_tenant_id (str): Tenant ID for SharePoint authentication.
14
+ sharepoint_tenant_name (str): Tenant name for SharePoint.
15
+ sharepoint_site_name (str): Site name in SharePoint.
16
+ local_backup_source_path (str): Local path to backup.
17
+ """
18
+
19
+ sharepoint_client_id: str
20
+ sharepoint_client_secret: str
21
+ sharepoint_tenant_id: str
22
+ sharepoint_tenant_name: str
23
+ sharepoint_site_name: str
24
+ logger: Logger
@@ -0,0 +1,11 @@
1
+ from dataclasses import dataclass
2
+ from logging import Logger
3
+
4
+
5
+ @dataclass
6
+ class SaviiaNetcameraConfig:
7
+ username: str
8
+ password: str
9
+ protocol: str
10
+ logger: Logger
11
+ destination_path: str = "/"
@@ -0,0 +1,21 @@
1
+ from dataclasses import dataclass
2
+ from logging import Logger
3
+
4
+
5
+ @dataclass
6
+ class SaviiaShakesConfig:
7
+ """
8
+ Configuration for Raspberry shakes activities as Miniseed extraction, photo record and video record.
9
+
10
+ Attributes:
11
+ sftp_user (str): Username for SFTP Client connection
12
+ sftp_password (str): Password for SFTP Client connection
13
+ sftp_port (str): SFTP Server Port. Default port is 22.
14
+ ssh_key_path (str): Path to the SSH Private key for client-side authentication.
15
+ """
16
+
17
+ sftp_user: str
18
+ sftp_password: str
19
+ ssh_key_path: str
20
+ logger: Logger
21
+ sftp_port: int = 22
@@ -0,0 +1,31 @@
1
+ from dataclasses import dataclass
2
+ from logging import Logger
3
+
4
+
5
+ @dataclass
6
+ class SaviiaThiesConfig:
7
+ """
8
+ Configuration for Saviia Thies.
9
+
10
+ Attributes:
11
+ ftp_port (int): Port number of the FTP server.
12
+ ftp_host (str): Hostname or IP address of the FTP server.
13
+ ftp_user (str): Username for the FTP server.
14
+ ftp_password (str): Password for the FTP server.
15
+ sharepoint_client_id (str): Client ID for SharePoint authentication.
16
+ sharepoint_client_secret (str): Client secret for SharePoint authentication.
17
+ sharepoint_tenant_id (str): Tenant ID for SharePoint authentication.
18
+ sharepoint_tenant_name (str): Tenant name for SharePoint.
19
+ sharepoint_site_name (str): Site name in SharePoint.
20
+ """
21
+
22
+ ftp_host: str
23
+ ftp_port: int
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
31
+ logger: Logger
@@ -43,6 +43,18 @@ class SharePointFetchingError(Exception):
43
43
  return self.reason.__str__()
44
44
 
45
45
 
46
+ class SharePointDirectoryError(Exception):
47
+ def __init__(self, *args, reason):
48
+ super().__init__(*args, reason)
49
+ self.reason = reason
50
+
51
+ def __str__(self):
52
+ return (
53
+ "An error occurred while fetching the folders from Microsoft SharePoint. "
54
+ + self.reason.__str__()
55
+ )
56
+
57
+
46
58
  class SharePointUploadError(Exception):
47
59
  """Raised when there is an error uploading files to the Microsoft SharePoint folder."""
48
60
 
@@ -91,3 +103,11 @@ class BackupEmptyError(Exception):
91
103
 
92
104
  def __str__(self):
93
105
  return "The local backup folder is empty. "
106
+
107
+
108
+ class ShakesNoContentError(Exception):
109
+ def __init__(self, *args):
110
+ super().__init__(*args)
111
+
112
+ def __str__(self):
113
+ return "All the miniSEED files have been downloaded and are in the local directory."
@@ -0,0 +1,7 @@
1
+ class NetcameraConnectionError(Exception):
2
+ def __init__(self, *args, reason):
3
+ super().__init__(*args, reason)
4
+ self.reason = reason
5
+
6
+ def __str__(self):
7
+ return "Netcamera Connection failed. " + self.reason.__str__()
@@ -15,3 +15,12 @@ class SharepointClientError(Exception):
15
15
  class FtpClientError(Exception):
16
16
  def __str__(self):
17
17
  return "Ftp Client initialization fails."
18
+
19
+
20
+ class SftpClientError(Exception):
21
+ def __init__(self, *args, reason):
22
+ super().__init__(*args, reason)
23
+ self.reason = reason
24
+
25
+ def __str__(self):
26
+ return "SFTP Client initialization fails." + self.reason.__str__()
@@ -0,0 +1,4 @@
1
+ from .directory_client import DirectoryClient
2
+ from .types.directory_client_types import DirectoryClientArgs
3
+
4
+ __all__ = ["DirectoryClient", "DirectoryClientArgs"]
@@ -0,0 +1,55 @@
1
+ from saviialib.libs.directory_client.directory_client_contract import (
2
+ DirectoryClientContract,
3
+ )
4
+ import os
5
+ import asyncio
6
+
7
+
8
+ class OsClient(DirectoryClientContract):
9
+ @staticmethod
10
+ def join_paths(*paths: str) -> str:
11
+ return os.path.join(*paths)
12
+
13
+ @staticmethod
14
+ async def path_exists(path: str) -> bool:
15
+ return await asyncio.to_thread(os.path.exists, path)
16
+
17
+ @staticmethod
18
+ async def listdir(path: str, more_info: bool = False) -> list:
19
+ def _listdir_with_size(path):
20
+ items = []
21
+ for name in os.listdir(path):
22
+ full_path = os.path.join(path, name)
23
+ is_dir = os.path.isdir(full_path)
24
+ size = os.stat(full_path).st_size if not is_dir else 0
25
+ items.append((name, size))
26
+ return items
27
+
28
+ if more_info:
29
+ return await asyncio.to_thread(_listdir_with_size, path)
30
+ return await asyncio.to_thread(os.listdir, path)
31
+
32
+ @staticmethod
33
+ async def isdir(path: str) -> bool:
34
+ return await asyncio.to_thread(os.path.isdir, path)
35
+
36
+ @staticmethod
37
+ async def makedirs(path: str) -> None:
38
+ return await asyncio.to_thread(os.makedirs, path, exist_ok=True)
39
+
40
+ @staticmethod
41
+ async def remove_file(path: str) -> None:
42
+ if await asyncio.to_thread(os.path.exists, path):
43
+ await asyncio.to_thread(os.remove, path)
44
+
45
+ @staticmethod
46
+ async def walk(path: str):
47
+ return await asyncio.to_thread(os.walk, path)
48
+
49
+ @staticmethod
50
+ def relative_path(full_path: str, base_folder: str):
51
+ return os.path.relpath(full_path, base_folder)
52
+
53
+ @staticmethod
54
+ def get_basename(path: str):
55
+ return os.path.basename(path)
@@ -0,0 +1,44 @@
1
+ from .client.os_client import OsClient
2
+ from .directory_client_contract import DirectoryClientContract
3
+ from .types.directory_client_types import DirectoryClientArgs
4
+ from typing import Iterator
5
+
6
+
7
+ class DirectoryClient(DirectoryClientContract):
8
+ CLIENTS = {"os_client"}
9
+
10
+ def __init__(self, args: DirectoryClientArgs) -> None:
11
+ if args.client_name not in DirectoryClient.CLIENTS:
12
+ msg = f"Unsupported client {args.client_name}"
13
+ raise KeyError(msg)
14
+
15
+ if args.client_name == "os_client":
16
+ self.client_obj = OsClient()
17
+ self.client_name = args.client_name
18
+
19
+ def join_paths(self, *paths: str) -> str:
20
+ return self.client_obj.join_paths(*paths)
21
+
22
+ async def path_exists(self, path: str) -> bool:
23
+ return await self.client_obj.path_exists(path)
24
+
25
+ async def listdir(self, path: str, more_info: bool = False) -> list:
26
+ return await self.client_obj.listdir(path, more_info)
27
+
28
+ async def isdir(self, path: str) -> bool:
29
+ return await self.client_obj.isdir(path)
30
+
31
+ async def makedirs(self, path: str) -> None:
32
+ return await self.client_obj.makedirs(path)
33
+
34
+ async def remove_file(self, path: str) -> None:
35
+ return await self.client_obj.remove_file(path)
36
+
37
+ async def walk(self, path: str) -> Iterator:
38
+ return await self.client_obj.walk(path)
39
+
40
+ def relative_path(self, full_path: str, base_folder: str):
41
+ return self.client_obj.relative_path(full_path, base_folder)
42
+
43
+ def get_basename(self, path: str):
44
+ return self.client_obj.get_basename(path)
@@ -0,0 +1,40 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Iterator
3
+
4
+
5
+ class DirectoryClientContract(ABC):
6
+ @abstractmethod
7
+ def join_paths(self, *paths: str) -> str:
8
+ pass
9
+
10
+ @abstractmethod
11
+ async def path_exists(self, path: str) -> bool:
12
+ pass
13
+
14
+ @abstractmethod
15
+ async def listdir(self, path: str, more_info: bool = False) -> list:
16
+ pass
17
+
18
+ @abstractmethod
19
+ async def isdir(self, path) -> bool:
20
+ pass
21
+
22
+ @abstractmethod
23
+ async def makedirs(self, path: str) -> None:
24
+ pass
25
+
26
+ @abstractmethod
27
+ async def remove_file(self, path: str) -> None:
28
+ pass
29
+
30
+ @abstractmethod
31
+ async def walk(self, path: str) -> Iterator:
32
+ pass
33
+
34
+ @abstractmethod
35
+ def relative_path(self, full_path: str, base_folder: str):
36
+ pass
37
+
38
+ @abstractmethod
39
+ def get_basename(self, path: str):
40
+ pass
@@ -0,0 +1,6 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class DirectoryClientArgs:
6
+ client_name: str = "os_client"
@@ -0,0 +1,8 @@
1
+ from .ffmpeg_client import (
2
+ FfmpegClient,
3
+ FfmpegClientInitArgs,
4
+ RecordPhotoArgs,
5
+ RecordVideoArgs,
6
+ )
7
+
8
+ __all__ = ["FfmpegClient", "FfmpegClientInitArgs", "RecordPhotoArgs", "RecordVideoArgs"]
@@ -0,0 +1,101 @@
1
+ from saviialib.libs.ffmpeg_client.types.ffmpeg_client_types import (
2
+ FfmpegClientInitArgs,
3
+ RecordPhotoArgs,
4
+ RecordVideoArgs,
5
+ )
6
+ from saviialib.libs.ffmpeg_client.ffmpeg_client_contract import FfmpegClientContract
7
+ from typing import List
8
+ import asyncio
9
+ import shutil
10
+
11
+ from saviialib.libs.directory_client import DirectoryClient, DirectoryClientArgs
12
+ from saviialib.libs.zero_dependency.utils.datetime_utils import today, datetime_to_str
13
+
14
+
15
+ class FfmpegAsyncioClient(FfmpegClientContract):
16
+ def __init__(self, args: FfmpegClientInitArgs) -> None:
17
+ self.dir_client = DirectoryClient(DirectoryClientArgs("os_client"))
18
+
19
+ def _setup_io_args(
20
+ self,
21
+ rtsp_user: str,
22
+ rtsp_pwd: str,
23
+ ip: str,
24
+ dest_path: str,
25
+ record_prefix: str,
26
+ record_type: str,
27
+ ):
28
+ input_arg = f"rtsp://{rtsp_user}:{rtsp_pwd}@{ip}/stream1"
29
+ output_file = (
30
+ record_prefix
31
+ + "_"
32
+ + datetime_to_str(today(), date_format="%m-%d-%Y_%H-%M-%S")
33
+ + f".{record_type}"
34
+ )
35
+ output_arg = self.dir_client.join_paths(dest_path, output_file)
36
+ return input_arg, output_arg
37
+
38
+ async def _ensure_ffmpeg_available(self):
39
+ if shutil.which("ffmpeg"):
40
+ return
41
+ install_cmd = ["apk", "add", "ffmpeg"] # Only for Home Assistant OS
42
+ process = await asyncio.create_subprocess_shell(
43
+ *install_cmd,
44
+ stdout=asyncio.subprocess.DEVNULL,
45
+ stdin=asyncio.subprocess.PIPE,
46
+ )
47
+ _, stderr = await process.communicate()
48
+ if process.returncode != 0:
49
+ raise ConnectionAbortedError("Failed to install ffmpeg: ", stderr.decode())
50
+
51
+ async def _setup_command(
52
+ self, input_arg: str, output_arg: str, extra: dict
53
+ ) -> List[str]:
54
+ await self._ensure_ffmpeg_available() # Validate ffmpeg module is installed.
55
+ cmd = ["ffmpeg", "-y", "-i", input_arg, output_arg]
56
+ for k, v in extra.values():
57
+ cmd.insert(-1, k)
58
+ cmd.insert(-1, v)
59
+ return list(map(str, cmd))
60
+
61
+ async def record_video(self, args: RecordVideoArgs):
62
+ input_arg, output_arg = self._setup_io_args(
63
+ args.rtsp_user,
64
+ args.rtsp_password,
65
+ args.ip_address,
66
+ args.destination_path,
67
+ "Video",
68
+ args.extension,
69
+ )
70
+ cmd = await self._setup_command(
71
+ input_arg, output_arg, extra={"-t": args.duration}
72
+ )
73
+ process = await asyncio.create_subprocess_exec(
74
+ *cmd, stderr=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE
75
+ )
76
+ _, stderr = await process.communicate()
77
+ if process.returncode != 0:
78
+ raise ConnectionError(
79
+ "Unexpected error while recording the video: ", stderr.decode()
80
+ )
81
+
82
+ async def record_photo(self, args: RecordPhotoArgs):
83
+ input_arg, output_arg = self._setup_io_args(
84
+ args.rtsp_user,
85
+ args.rtsp_password,
86
+ args.ip_address,
87
+ args.destination_path,
88
+ "Photo",
89
+ args.extension,
90
+ )
91
+ cmd = await self._setup_command(
92
+ input_arg, output_arg, extra={"-frames:v": args.frames}
93
+ )
94
+ process = await asyncio.create_subprocess_exec(
95
+ *cmd, stderr=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE
96
+ )
97
+ _, stderr = await process.communicate()
98
+ if process.returncode != 0:
99
+ raise ConnectionError(
100
+ "Unexpected error while recording the photo: ", stderr.decode()
101
+ )
@@ -0,0 +1,25 @@
1
+ from .ffmpeg_client_contract import FfmpegClientContract
2
+ from .types.ffmpeg_client_types import (
3
+ RecordPhotoArgs,
4
+ RecordVideoArgs,
5
+ FfmpegClientInitArgs,
6
+ )
7
+ from .clients.ffmpeg_asyncio_client import FfmpegAsyncioClient
8
+
9
+
10
+ class FfmpegClient(FfmpegClientContract):
11
+ CLIENTS = {"ffmpeg_asyncio"}
12
+
13
+ def __init__(self, args: FfmpegClientInitArgs) -> None:
14
+ if args.client_name not in FfmpegClient.CLIENTS:
15
+ msg = f"Unsupported client {args.client_name}"
16
+ raise KeyError(msg)
17
+ if args.client_name == "ffmpeg_asyncio":
18
+ self.client_obj = FfmpegAsyncioClient(args)
19
+ self.client_name = args.client_name
20
+
21
+ def record_photo(self, args: RecordPhotoArgs):
22
+ return self.client_obj.record_photo(args)
23
+
24
+ def record_video(self, args: RecordVideoArgs):
25
+ return self.client_obj.record_video(args)
@@ -0,0 +1,12 @@
1
+ from abc import ABC, abstractmethod
2
+ from .types.ffmpeg_client_types import RecordPhotoArgs, RecordVideoArgs
3
+
4
+
5
+ class FfmpegClientContract(ABC):
6
+ @abstractmethod
7
+ async def record_photo(self, args: RecordPhotoArgs):
8
+ pass
9
+
10
+ @abstractmethod
11
+ async def record_video(self, args: RecordVideoArgs):
12
+ pass
@@ -0,0 +1,28 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class FfmpegClientInitArgs:
6
+ client_name: str
7
+
8
+
9
+ @dataclass
10
+ class RecordPhotoArgs:
11
+ ip_address: str
12
+ destination_path: str
13
+ rtsp_user: str
14
+ rtsp_password: str
15
+ port: str
16
+ extension: str
17
+ frames: int
18
+
19
+
20
+ @dataclass
21
+ class RecordVideoArgs:
22
+ destination_path: str
23
+ ip_address: str
24
+ port: str
25
+ rtsp_user: str
26
+ rtsp_password: str
27
+ extension: str
28
+ duration: int
@@ -1,4 +1,4 @@
1
1
  from .files_client import FilesClient
2
- from .types.files_client_types import FilesClientInitArgs, ReadArgs
2
+ from .types.files_client_types import FilesClientInitArgs, ReadArgs, WriteArgs
3
3
 
4
- __all__ = ["FilesClient", "FilesClientInitArgs", "ReadArgs"]
4
+ __all__ = ["FilesClient", "FilesClientInitArgs", "ReadArgs", "WriteArgs"]