Encryptors 2.36__tar.gz → 2.38__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.
Files changed (88) hide show
  1. {encryptors-2.36 → encryptors-2.38}/PKG-INFO +2 -1
  2. {encryptors-2.36 → encryptors-2.38}/setup.py +2 -1
  3. {encryptors-2.36 → encryptors-2.38}/src/Encryptors.egg-info/PKG-INFO +2 -1
  4. {encryptors-2.36 → encryptors-2.38}/src/Encryptors.egg-info/requires.txt +1 -0
  5. encryptors-2.38/src/Osdental/Cli/__init__.py +118 -0
  6. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Database/BaseRepository.py +34 -20
  7. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Graphql/Extensions/AuditExtension.py +3 -0
  8. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +4 -1
  9. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Enums/Message.py +0 -1
  10. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/FileMetaData.py +1 -1
  11. encryptors-2.36/src/Osdental/Cli/__init__.py +0 -505
  12. {encryptors-2.36 → encryptors-2.38}/README.md +0 -0
  13. {encryptors-2.36 → encryptors-2.38}/setup.cfg +0 -0
  14. {encryptors-2.36 → encryptors-2.38}/src/Encryptors.egg-info/SOURCES.txt +0 -0
  15. {encryptors-2.36 → encryptors-2.38}/src/Encryptors.egg-info/dependency_links.txt +0 -0
  16. {encryptors-2.36 → encryptors-2.38}/src/Encryptors.egg-info/entry_points.txt +0 -0
  17. {encryptors-2.36 → encryptors-2.38}/src/Encryptors.egg-info/top_level.txt +0 -0
  18. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Database/Connection.py +0 -0
  19. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Database/__init__.py +0 -0
  20. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Decorators/Grpc.py +0 -0
  21. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Decorators/Retry.py +0 -0
  22. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Decorators/SecureResolver.py +0 -0
  23. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Decorators/SqlDataNormalizer.py +0 -0
  24. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Decorators/__init__.py +0 -0
  25. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Encryptor/Aes.py +0 -0
  26. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Encryptor/Argon2.py +0 -0
  27. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Encryptor/Bcrypt.py +0 -0
  28. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Encryptor/Jwt.py +0 -0
  29. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Encryptor/Rsa.py +0 -0
  30. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Encryptor/Sha512.py +0 -0
  31. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Encryptor/__init__.py +0 -0
  32. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Exception/ControlledException.py +0 -0
  33. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Exception/__init__.py +0 -0
  34. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Graphql/Extensions/__init__.py +0 -0
  35. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Graphql/Models/__init__.py +0 -0
  36. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Graphql/_Exceptions/__init__.py +0 -0
  37. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Graphql/_Helpers/_AuditHelper.py +0 -0
  38. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -0
  39. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Graphql/_Helpers/_TokenService.py +0 -0
  40. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Graphql/_Helpers/__init__.py +0 -0
  41. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Graphql/__init__.py +0 -0
  42. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Helpers/AuditDispatcher.py +0 -0
  43. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Helpers/KeyVaultService.py +0 -0
  44. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Helpers/__init__.py +0 -0
  45. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Http/APIClient.py +0 -0
  46. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Http/_Exceptions.py +0 -0
  47. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Http/__init__.py +0 -0
  48. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
  49. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Messaging/Kafka.py +0 -0
  50. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Messaging/RabbitMQ.py +0 -0
  51. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Messaging/__init__.py +0 -0
  52. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Models/AuditConfig.py +0 -0
  53. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Models/Catalog.py +0 -0
  54. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Models/Legacy.py +0 -0
  55. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Models/Response.py +0 -0
  56. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Models/Token.py +0 -0
  57. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Models/_Audit.py +0 -0
  58. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Models/__init__.py +0 -0
  59. {encryptors-2.36 → encryptors-2.38}/src/Osdental/RedisCache/Redis.py +0 -0
  60. {encryptors-2.36 → encryptors-2.38}/src/Osdental/RedisCache/__init__.py +0 -0
  61. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Rest/Context/RequestContext.py +0 -0
  62. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Rest/Context/__init__.py +0 -0
  63. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -0
  64. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Rest/Middlewares/__init__.py +0 -0
  65. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Rest/__init__.py +0 -0
  66. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Enums/Code.py +0 -0
  67. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Enums/Constant.py +0 -0
  68. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Enums/FileType.py +0 -0
  69. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Enums/GrahpqlOperation.py +0 -0
  70. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Enums/Profile.py +0 -0
  71. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Enums/__init__.py +0 -0
  72. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Logger.py +0 -0
  73. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/CaseConverter.py +0 -0
  74. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/CodeGenerator.py +0 -0
  75. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/DataNormalizer.py +0 -0
  76. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/DataUtils.py +0 -0
  77. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/DateUtils.py +0 -0
  78. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/HashValidator.py +0 -0
  79. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/Mapper.py +0 -0
  80. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/PasswordGenerator.py +0 -0
  81. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/QueryGenerator.py +0 -0
  82. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/TextProcessor.py +0 -0
  83. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/Utils/__init__.py +0 -0
  84. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Shared/__init__.py +0 -0
  85. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
  86. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Storage/S3Storage.py +0 -0
  87. {encryptors-2.36 → encryptors-2.38}/src/Osdental/Storage/__init__.py +0 -0
  88. {encryptors-2.36 → encryptors-2.38}/src/Osdental/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.36
3
+ Version: 2.38
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -35,6 +35,7 @@ Requires-Dist: pydantic_core==2.41.4
35
35
  Requires-Dist: PyJWT==2.10.1
36
36
  Requires-Dist: aioodbc==0.5.0
37
37
  Requires-Dist: python-dotenv==1.0.1
38
+ Requires-Dist: pydantic_settings==2.13.1
38
39
  Requires-Dist: requests==2.32.3
39
40
  Requires-Dist: six==1.17.0
40
41
  Requires-Dist: sniffio==1.3.1
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="Encryptors",
5
- version="2.36",
5
+ version="2.38",
6
6
  author="OSDental LLC",
7
7
  author_email="support@osdental.ai",
8
8
  description="End-to-end algorithm library",
@@ -43,6 +43,7 @@ setup(
43
43
  "PyJWT==2.10.1",
44
44
  "aioodbc==0.5.0",
45
45
  "python-dotenv==1.0.1",
46
+ "pydantic_settings==2.13.1",
46
47
  "requests==2.32.3",
47
48
  "six==1.17.0",
48
49
  "sniffio==1.3.1",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.36
3
+ Version: 2.38
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -35,6 +35,7 @@ Requires-Dist: pydantic_core==2.41.4
35
35
  Requires-Dist: PyJWT==2.10.1
36
36
  Requires-Dist: aioodbc==0.5.0
37
37
  Requires-Dist: python-dotenv==1.0.1
38
+ Requires-Dist: pydantic_settings==2.13.1
38
39
  Requires-Dist: requests==2.32.3
39
40
  Requires-Dist: six==1.17.0
40
41
  Requires-Dist: sniffio==1.3.1
@@ -24,6 +24,7 @@ pydantic_core==2.41.4
24
24
  PyJWT==2.10.1
25
25
  aioodbc==0.5.0
26
26
  python-dotenv==1.0.1
27
+ pydantic_settings==2.13.1
27
28
  requests==2.32.3
28
29
  six==1.17.0
29
30
  sniffio==1.3.1
@@ -0,0 +1,118 @@
1
+ import os
2
+ import sys
3
+ import subprocess
4
+ import platform
5
+ import click
6
+ from Osdental.Shared.Logger import logger
7
+ from Osdental.Shared.Enums.Message import Message
8
+
9
+
10
+ @click.group()
11
+ def cli():
12
+ """Custom commands to manage the project."""
13
+ pass
14
+
15
+ @cli.command()
16
+ def clean():
17
+ """Borrar todos los __pycache__."""
18
+ if platform.system() == 'Windows':
19
+ subprocess.run('for /d /r . %d in (__pycache__) do @if exist "%d" rd /s/q "%d"', shell=True)
20
+ else:
21
+ subprocess.run("find . -name '__pycache__' -type d -exec rm -rf {} +", shell=True)
22
+
23
+ logger.info(Message.PYCACHE_CLEANUP_SUCCESS_MSG)
24
+
25
+ @cli.command()
26
+ @click.argument('port')
27
+ def start(port: int):
28
+ """Start the FastAPI server.."""
29
+ try:
30
+ subprocess.run(['uvicorn', 'app:app', '--port', str(port), '--reload'], check=True)
31
+ except subprocess.CalledProcessError as e:
32
+ logger.error(f'{Message.SERVER_NETWORK_ACCESS_ERROR_MSG}: {e}')
33
+
34
+ @cli.command()
35
+ @click.argument('port')
36
+ def serve(port: int):
37
+ """Set up the FastAPI server accessible from any machine."""
38
+ try:
39
+ subprocess.run(['uvicorn', 'app:app', '--host', '0.0.0.0', '--port', str(port), '--reload'], check=True)
40
+ except subprocess.CalledProcessError as e:
41
+ logger.error(f'{Message.SERVER_NETWORK_ACCESS_ERROR_MSG}: {e}')
42
+
43
+ @cli.command("clean-redis")
44
+ @click.argument('redis_env')
45
+ async def clean_redis(redis_env: str):
46
+ try:
47
+ from Osdental.RedisCache.Redis import RedisCacheAsync
48
+ redis_url = os.getenv(redis_env)
49
+ if not redis_url:
50
+ logger.warning(f'Environment variable not found: {redis_env}')
51
+ return
52
+
53
+ redis = RedisCacheAsync(redis_url=redis_url)
54
+ await redis.flush()
55
+ logger.info(Message.REDIS_CLEANUP_SUCCESS_MSG)
56
+ except Exception as e:
57
+ logger.error(f'{Message.REDIS_CLEANUP_ERROR_MSG}: {e}')
58
+
59
+ @cli.command(name='proto-files')
60
+ @click.argument('name')
61
+ def proto_files(name: str):
62
+ proto_dir = os.path.join('src', 'Infrastructure', 'Grpc', 'Proto').replace('\\', '/')
63
+ gen_dir = os.path.join('src', 'Infrastructure', 'Grpc', 'Generated').replace('\\', '/')
64
+
65
+ proto_path = os.path.join(proto_dir, f'{name}.proto').replace('\\', '/')
66
+ common_path = os.path.join(proto_dir, 'Common.proto').replace('\\', '/')
67
+
68
+ common_py = os.path.join(gen_dir, 'Common_pb2.py').replace('\\', '/')
69
+ common_grpc_py = os.path.join(gen_dir, 'Common_pb2_grpc.py').replace('\\', '/')
70
+
71
+ proto_files = [proto_path]
72
+ if not (os.path.exists(common_py) and os.path.exists(common_grpc_py)):
73
+ proto_files.append(common_path)
74
+
75
+ cmd = [
76
+ sys.executable, "-m", "grpc_tools.protoc",
77
+ f"-I={proto_dir}",
78
+ f"--python_out={gen_dir}",
79
+ f"--grpc_python_out={gen_dir}",
80
+ *proto_files
81
+ ]
82
+
83
+ subprocess.run(cmd, shell=False, check=True)
84
+ logger.info(Message.PROTO_FILES_GENERATED_MSG)
85
+
86
+ @cli.command('run-server')
87
+ @click.option('--host', default="0.0.0.0", help='Host where to set up the server')
88
+ @click.option('--port', default=5000, help='Server port')
89
+ @click.option('--workers', default=4, help='Number of workers')
90
+ def run_server(host, port, workers):
91
+ """
92
+ Launch the application with uvicorn on Windows or gunicorn on other systems
93
+ :host
94
+ :port
95
+ :workers
96
+ """
97
+ if sys.platform.startswith('win'):
98
+ cmd = [
99
+ sys.executable, '-m', 'uvicorn',
100
+ 'app:app',
101
+ '--host', host,
102
+ '--port', str(port),
103
+ '--workers', str(workers)
104
+ ]
105
+ else:
106
+ cmd = [
107
+ sys.executable, '-m', 'gunicorn',
108
+ 'app:app',
109
+ '-k', 'uvicorn.workers.UvicornWorker',
110
+ '--bind', f'{host}:{port}',
111
+ '--workers', str(workers)
112
+ ]
113
+
114
+ logger.info(f'Running: {' '.join(cmd)}')
115
+ subprocess.run(cmd)
116
+
117
+ if __name__ == "__main__":
118
+ cli()
@@ -1,7 +1,7 @@
1
1
  from typing import Any
2
2
  from sqlalchemy import text
3
3
  from sqlalchemy.ext.asyncio import AsyncSession
4
- from Osdental.Exception.ControlledException import DatabaseException
4
+ from Osdental.Exception.ControlledException import DatabaseException, OSDException
5
5
  from Osdental.Shared.Utils.DataNormalizer import normalize
6
6
 
7
7
  class BaseRepository:
@@ -23,6 +23,7 @@ class BaseRepository:
23
23
  many: bool = False,
24
24
  as_dict: bool = False
25
25
  ) -> Any:
26
+
26
27
  result = await self.async_session.execute(
27
28
  text(query),
28
29
  params or {}
@@ -34,13 +35,11 @@ class BaseRepository:
34
35
  else result.mappings().first()
35
36
  )
36
37
 
37
- if not as_dict or data is None:
38
- return data
39
-
40
- if many:
38
+ if as_dict and data is not None:
41
39
  return normalize(data)
42
40
 
43
- return normalize(data)
41
+ return data
42
+
44
43
 
45
44
  async def _execute_command(
46
45
  self,
@@ -48,8 +47,10 @@ class BaseRepository:
48
47
  params: dict | None = None,
49
48
  *,
50
49
  validate: bool = True,
51
- success_codes: str | int | tuple[str | int, ...] | None = None
52
- ):
50
+ success_codes: str | int | tuple[str | int, ...] | None = None,
51
+ as_dict: bool = False
52
+ ) -> Any:
53
+
53
54
  result = await self.async_session.execute(
54
55
  text(query),
55
56
  params or {}
@@ -58,7 +59,8 @@ class BaseRepository:
58
59
  row = result.mappings().first()
59
60
 
60
61
  if not validate or not row:
61
- return row
62
+ return normalize(row) if as_dict else row
63
+
62
64
 
63
65
  status_code = self._get_status_value(
64
66
  row,
@@ -82,17 +84,29 @@ class BaseRepository:
82
84
  error="Status code missing",
83
85
  status_code=500
84
86
  )
85
- return row
86
87
 
87
- # Normalizamos a tupla
88
- if not isinstance(success_codes, tuple):
89
- success_codes = (success_codes,)
88
+ else:
89
+
90
+ if not isinstance(success_codes, tuple):
91
+ success_codes = (success_codes,)
92
+
93
+ if status_code not in success_codes:
94
+ raise DatabaseException(
95
+ message=status_message,
96
+ error=status_message,
97
+ status_code=status_code
98
+ )
90
99
 
91
- if status_code not in success_codes:
92
- raise DatabaseException(
93
- message=status_message,
94
- error=status_message,
95
- status_code=status_code
96
- )
100
+ return normalize(row) if as_dict else row
101
+
97
102
 
98
- return row
103
+ async def _execute_scalar(
104
+ self,
105
+ query: str,
106
+ params: dict | None = None
107
+ ) -> Any | None:
108
+ result = await self.async_session.execute(
109
+ text(query),
110
+ params or {}
111
+ )
112
+ return result.scalar_one_or_none()
@@ -102,7 +102,10 @@ class AuditExtension(Extension):
102
102
  self.errors = errors
103
103
 
104
104
  def request_finished(self, context):
105
+ if not self.request_payload: return
106
+
105
107
  query = self.request_payload.get("query", "")
108
+
106
109
  if "__schema" in query or "__type" in query: return
107
110
 
108
111
  if not hasattr(context, "audit_plain_response"): return
@@ -37,7 +37,10 @@ class TenantPolicy:
37
37
  return token
38
38
 
39
39
  # If it comes by request, it is taken as priority
40
- external_enterprise_req = decrypted_payload.get("idExternalEnterprise")
40
+ external_enterprise_req = (
41
+ decrypted_payload.get("idExternalEnterprise")
42
+ if decrypted_payload else None
43
+ )
41
44
  if external_enterprise_req and token:
42
45
  token.id_external_enterprise = external_enterprise_req
43
46
 
@@ -21,7 +21,6 @@ class Message(StrEnum):
21
21
  AES_KEY_USER_REQUIRED_MSG = 'AES key user cannot be empty.'
22
22
  AES_KEY_AUTH_REQUIRED_MSG = 'AES key auth cannot be empty.'
23
23
  MISSING_FIELD_ERROR_MSG = 'A required field is missing. Please review the data.'
24
- ID_CDATA_INTEGRATION_REQUIRED = 'Cdata integration cannot be empty or null'
25
24
  EXP_TIME_REQUIRED = 'Expiration time (cdata integration) cannot be empty or null'
26
25
  KEY_PRIVATE_REQUIRED = 'Key private cannot be empty or null'
27
26
  SUB_ACCOUNT_REQUIRED = 'Sub account parameter is required'
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import mimetypes
3
3
  from Osdental.Exception.ControlledException import ValidationDataException
4
- from Osdental.Shared.Message import Message
4
+ from Osdental.Shared.Enums.Message import Message
5
5
  from Osdental.Shared.Enums.FileType import FileType
6
6
 
7
7
  class FileMetaData:
@@ -1,505 +0,0 @@
1
- import os
2
- import sys
3
- import subprocess
4
- import platform
5
- import click
6
- from Osdental.Shared.Logger import logger
7
- from Osdental.Shared.Enums.Message import Message
8
- from Osdental.Shared.Utils.CaseConverter import CaseConverter
9
-
10
- SRC_PATH = 'src'
11
- APP_PATH = os.path.join(SRC_PATH, 'Application')
12
- DOMAIN_PATH = os.path.join(SRC_PATH, 'Domain')
13
- INFRA_PATH = os.path.join(SRC_PATH, 'Infrastructure')
14
- GRAPHQL_PATH = os.path.join(INFRA_PATH, 'Graphql')
15
- GRPC_PATH = os.path.join(INFRA_PATH, 'Grpc')
16
- RESOLVERS_PATH = os.path.join(GRAPHQL_PATH, 'Resolvers')
17
- SCHEMAS_PATH = os.path.join(GRAPHQL_PATH, 'Schemas')
18
-
19
- @click.group()
20
- def cli():
21
- """Comandos personalizados para gestionar el proyecto."""
22
- pass
23
-
24
- @cli.command()
25
- def clean():
26
- """Borrar todos los __pycache__."""
27
- if platform.system() == 'Windows':
28
- subprocess.run('for /d /r . %d in (__pycache__) do @if exist "%d" rd /s/q "%d"', shell=True)
29
- else:
30
- subprocess.run("find . -name '__pycache__' -type d -exec rm -rf {} +", shell=True)
31
-
32
- logger.info(Message.PYCACHE_CLEANUP_SUCCESS_MSG)
33
-
34
-
35
- @cli.command(name='start-app')
36
- @click.argument('app')
37
- def start_app(app: str):
38
- """Crear un servicio con estructura hexagonal y CRUD básico."""
39
- app = CaseConverter.snake_to_pascal(app)
40
- app_upper = app.upper()
41
- if '-' in app:
42
- part_one, part_two = tuple(app.split('-'))
43
- app = part_one + part_two
44
- app_upper = f'{part_one}_{part_two}'.upper()
45
-
46
- name_method = CaseConverter.case_to_snake(app)
47
- data = 'data: Dict[str, Any]'
48
- token = 'token: AuthToken'
49
- api_type_response = 'Response!'
50
-
51
- directories = [
52
- os.path.join(SRC_PATH),
53
- os.path.join(APP_PATH, 'UseCases'),
54
- os.path.join(APP_PATH, 'Interfaces'),
55
- os.path.join(DOMAIN_PATH, 'Interfaces'),
56
- os.path.join(DOMAIN_PATH, 'Models'),
57
- os.path.join(RESOLVERS_PATH),
58
- os.path.join(SCHEMAS_PATH),
59
- os.path.join(SCHEMAS_PATH, app),
60
- os.path.join(GRPC_PATH, 'Proto'),
61
- os.path.join(GRPC_PATH, 'Generated'),
62
- os.path.join(GRPC_PATH, 'Server'),
63
- os.path.join(GRPC_PATH, 'Client'),
64
- os.path.join(GRPC_PATH, 'Servicer'),
65
- os.path.join(INFRA_PATH, 'Repositories', app)
66
- ]
67
-
68
- for directory in directories:
69
- os.makedirs(directory, exist_ok=True)
70
-
71
- # Contenidos CRUD
72
- use_case_interface_name = f'{app}UseCaseInterface'
73
- use_case_interface_content = f'''
74
- from abc import ABC, abstractmethod
75
- from typing import Dict, Any
76
- from Osdental.Models.Token import AuthToken
77
-
78
- class {use_case_interface_name}(ABC):
79
-
80
- @abstractmethod
81
- async def get_all_{name_method}(self, {token}, {data}) -> str: ...
82
-
83
- @abstractmethod
84
- async def get_{name_method}_by_id(self, {token}, {data}) -> str: ...
85
-
86
- @abstractmethod
87
- async def create_{name_method}(self, {token}, {data}) -> str: ...
88
-
89
- @abstractmethod
90
- async def update_{name_method}(self, {token}, {data}) -> str: ...
91
-
92
- @abstractmethod
93
- async def delete_{name_method}(self, {token}, {data}) -> str: ...
94
- '''
95
-
96
-
97
- use_case_content = f'''
98
- from typing import Dict, Any
99
- from Osdental.Decorators.DecryptedData import process_encrypted_data
100
- from Osdental.Models.Token import AuthToken
101
- from Osdental.Database.UnitOfWork import UnitOfWork
102
- from ..Interfaces.{use_case_interface_name} import {use_case_interface_name}
103
-
104
- class {app}UseCase({use_case_interface_name}):
105
-
106
- def __init__(self, unit_of_work: UnitOfWork):
107
- self.unit_of_work = unit_of_work
108
-
109
- @process_encrypted_data()
110
- async def get_all_{name_method}(self, {token}, {data}) -> str:
111
- async with self.unit_of_work() as uow:
112
- ...
113
-
114
- @process_encrypted_data()
115
- async def get_{name_method}_by_id(self, {token}, {data}) -> str:
116
- async with self.unit_of_work() as uow:
117
- ...
118
-
119
- @process_encrypted_data()
120
- async def create_{name_method}(self, {token}, {data}) -> str:
121
- async with self.unit_of_work() as uow:
122
- ...
123
-
124
- @process_encrypted_data()
125
- async def update_{name_method}(self, {token}, {data}) -> str:
126
- async with self.unit_of_work() as uow:
127
- ...
128
-
129
- @process_encrypted_data()
130
- async def delete_{name_method}(self, {token}, {data}) -> str:
131
- async with self.unit_of_work() as uow:
132
- ...
133
- '''
134
-
135
-
136
- repository_interface_name = f'{app}RepositoryInterface'
137
- repository_interface_content = f'''
138
- from abc import ABC, abstractmethod
139
- from sqlalchemy import text, RowMapping
140
- from typing import List, Dict, Any
141
-
142
- class {repository_interface_name}(ABC):
143
-
144
- @abstractmethod
145
- async def get_all_{name_method}(self, {data}) -> List[RowMapping]: ...
146
-
147
- @abstractmethod
148
- async def get_{name_method}_by_id(self, id: str) -> RowMapping: ...
149
-
150
- @abstractmethod
151
- async def create_{name_method}(self, {data}) -> str: ...
152
-
153
- @abstractmethod
154
- async def update_{name_method}(self, id: str, {data}) -> str: ...
155
-
156
- @abstractmethod
157
- async def delete_{name_method}(self, id: str) -> str: ...
158
- '''
159
-
160
-
161
- repository_content = f'''
162
- from typing import List, Dict, Any
163
- from sqlalchemy import text, RowMapping
164
- from sqlalchemy.ext.asyncio import AsyncSession
165
- from src.Domain.Interfaces.{repository_interface_name} import {repository_interface_name}
166
-
167
- class {app}Repository({repository_interface_name}):
168
-
169
- def __init__(self, session: AsyncSession):
170
- self.session = session
171
-
172
- async def get_all_{name_method}(self, {data}) -> List[RowMapping]: ...
173
-
174
- async def get_{name_method}_by_id(self, id: str) -> RowMapping: ...
175
-
176
- async def create_{name_method}(self, {data}) -> str: ...
177
-
178
- async def update_{name_method}(self, id: str, {data}) -> str: ...
179
-
180
- async def delete_{name_method}(self, id: str) -> str: ...
181
- '''
182
-
183
-
184
- query_graphql = f'''type Query {{
185
- getAll{app}(data: String!): {api_type_response}
186
- get{app}ById(data: String!): {api_type_response}
187
- }}
188
- '''
189
-
190
- mutation_graphql = f'''type Mutation {{
191
- create{app}(data: String!): {api_type_response}
192
- update{app}(data: String!): {api_type_response}
193
- delete{app}(data: String!): {api_type_response}
194
- }}
195
- '''
196
-
197
- resolver_content_init = f"""
198
- from .{app}Resolver import {app}Resolver
199
-
200
- {name_method}_query_resolvers = {{
201
- 'getAll{app}': {app}Resolver.resolve_get_all_{name_method},
202
- 'get{app}ById': {app}Resolver.resolve_get_{name_method}_by_id
203
- }}
204
-
205
- {name_method}_mutation_resolvers = {{
206
- 'create{app}': {app}Resolver.resolve_create_{name_method},
207
- 'update{app}': {app}Resolver.resolve_update_{name_method},
208
- 'delete{app}': {app}Resolver.resolve_delete_{name_method}
209
- }}
210
- """
211
-
212
- resolver_content = f'''
213
- from Osdental.Decorators.AuditLog import handle_audit_and_exception
214
- from Osdental.Database.Connection import Connection
215
- from Osdental.Database.UnitOfWork import UnitOfWork
216
- from src.Application.UseCases.{app}UseCase import {app}UseCase
217
- from src.Infrastructure.Repositories.{app}.{app}Repository import {app}Repository
218
-
219
- use_case = {app}UseCase({app}Repository)
220
-
221
- connection = Connection(...) # Enter DB URL
222
- unit_of_work = UnitOfWork(connection.get_session())
223
- use_case = {app}UseCase(unit_of_work)
224
-
225
- class {app}Resolver:
226
-
227
- @staticmethod
228
- @handle_audit_and_exception()
229
- async def resolve_get_all_{name_method}(_, info, data):
230
- return await use_case.get_all_{name_method}(info=info, aes_data=data)
231
-
232
- @staticmethod
233
- @handle_audit_and_exception()
234
- async def resolve_get_{name_method}_by_id(_, info, data):
235
- return await use_case.get_{name_method}_by_id(info=info, aes_data=data)
236
-
237
- @staticmethod
238
- @handle_audit_and_exception()
239
- async def resolve_create_{name_method}(_, info, data):
240
- return await use_case.create_{name_method}(info=info, aes_data=data)
241
-
242
- @staticmethod
243
- @handle_audit_and_exception()
244
- async def resolve_update_{name_method}(_, info, data):
245
- return await use_case.update_{name_method}(info=info, aes_data=data)
246
-
247
- @staticmethod
248
- @handle_audit_and_exception()
249
- async def resolve_delete_{name_method}(_, info, data):
250
- return await use_case.delete_{name_method}(info=info, aes_data=data)
251
- '''
252
-
253
- graphql_content_init = f'''
254
- from ariadne import gql
255
- from ariadne import QueryType, MutationType
256
- from ariadne import make_executable_schema
257
- from pathlib import Path
258
- from ..Graphql.Resolvers.{app} import {name_method}_query_resolvers, {name_method}_mutation_resolvers
259
-
260
- def load_schemas():
261
- schema_dir = Path(__file__).parent / 'Schemas'
262
- schemas = [schema.read_text() for schema in schema_dir.rglob('*.graphql')]
263
- return gql('\\n'.join(schemas))
264
-
265
- type_defs = load_schemas()
266
-
267
- query_resolvers = {{
268
- **{name_method}_query_resolvers,
269
- }}
270
-
271
- mutation_resolvers = {{
272
- **{name_method}_mutation_resolvers,
273
- }}
274
-
275
- query = QueryType()
276
- mutation = MutationType()
277
-
278
- for field, resolver in query_resolvers.items():
279
- query.set_field(field, resolver)
280
-
281
- for field, resolver in mutation_resolvers.items():
282
- mutation.set_field(field, resolver)
283
-
284
- # Executable Schema
285
- schema = make_executable_schema(type_defs, query, mutation)
286
- '''
287
-
288
-
289
- response_content = '''
290
- type Response {
291
- status: String
292
- message: String
293
- data: String
294
- }
295
- '''
296
-
297
- init_file = '__init__.py'
298
-
299
- repository_init_content = f"""
300
- from enum import StrEnum
301
-
302
- class {app}Query(StrEnum):
303
- GET_ALL_{app_upper} = ''' EXEC '''
304
-
305
- GET_{app_upper}_BY_ID = ''' EXEC '''
306
-
307
- CREATE_{app_upper} = ''' EXEC '''
308
-
309
- UPDATE_{app_upper} = ''' EXEC '''
310
-
311
- DELETE_{app_upper} = ''' EXEC '''
312
- """
313
-
314
- proto_app_content = f'''
315
- syntax = "proto3";
316
-
317
- package {app.lower()};
318
-
319
- import "Common.proto";
320
-
321
- // gRPC Service
322
- service {app}{{
323
- rpc Example (common.Request) returns (common.Response);
324
- }}
325
- '''
326
-
327
- common_proto = '''
328
- syntax = "proto3";
329
-
330
- package common;
331
-
332
- message Request { string data = 1; }
333
- message Response { string status=1; string message=2; string data=3; }
334
- message Empty {}
335
- '''
336
- server = '''
337
- from grpc.experimental import aio
338
- from grpc_reflection.v1alpha import reflection
339
-
340
- async def serve():
341
- server = aio.server()
342
-
343
- # Register each service
344
- ExampleService_pb2_grpc.add_ExampleServiceServicer_to_server(
345
- ExampleServicer(), server
346
- )
347
-
348
- # Register reflection
349
- SERVICE_NAMES = (
350
- Example.DESCRIPTOR.services_by_name['ExampleService'].full_name,
351
- reflection.SERVICE_NAME # List of services exposed via gRPC reflection
352
- )
353
- reflection.enable_server_reflection(SERVICE_NAMES, server)
354
- # Single port exposed
355
- server.add_insecure_port("[::]:50051") # Port must go in environment variables
356
-
357
- await server.start()
358
- await server.wait_for_termination()
359
- '''
360
-
361
- grpc_client = f'''
362
- import grpc
363
- from Osdental.Decorators.Grpc import with_grpc_metadata
364
- from Osdental.Models.Response import Response
365
- from src.Infrastructure.Grpc.Generated import Common_pb2
366
-
367
- class {app}Client:
368
-
369
- def __init__(self, host="localhost", port=50051):
370
- # Connecting to the gRPC server
371
- self.channel = grpc.aio.insecure_channel(f"{{host}}:{{port}}")
372
- self.stub = ExampleService_pb2_grpc.ExampleServiceStub(self.channel)
373
-
374
- # Implement your RPC methods to consume
375
- @with_grpc_metadata
376
- async def example(self, request, metadata) -> Response:
377
- request = Common_pb2.Request(data=request)
378
- return await self.stub.Example(request, metadata=metadata)
379
- '''
380
- files = {
381
- os.path.join(APP_PATH, 'UseCases', f'{app}UseCase.py'): use_case_content,
382
- os.path.join(APP_PATH, 'Interfaces', f'{app}UseCaseInterface.py'): use_case_interface_content,
383
- os.path.join(DOMAIN_PATH, 'Interfaces', f'{app}RepositoryInterface.py'): repository_interface_content,
384
- os.path.join(RESOLVERS_PATH, app, init_file): resolver_content_init,
385
- os.path.join(RESOLVERS_PATH, app, f'{app}Resolver.py'): resolver_content,
386
- os.path.join(GRAPHQL_PATH, init_file): graphql_content_init,
387
- os.path.join(SCHEMAS_PATH, app, 'Query.graphql'): query_graphql,
388
- os.path.join(SCHEMAS_PATH, app, 'Mutation.graphql'): mutation_graphql,
389
- os.path.join(SCHEMAS_PATH, 'Response.graphql'): response_content,
390
- os.path.join(INFRA_PATH, 'Repositories', app, init_file): repository_init_content,
391
- os.path.join(INFRA_PATH, 'Repositories', app, f'{app}Repository.py'): repository_content,
392
- os.path.join(GRPC_PATH, 'Proto', 'Common.proto'): common_proto,
393
- os.path.join(GRPC_PATH, 'Proto', f'{app}.proto'): proto_app_content,
394
- os.path.join(GRPC_PATH, 'Server', init_file): server,
395
- os.path.join(GRPC_PATH, 'Client', f'{app}Client.py'): grpc_client
396
- }
397
- for file_path in files:
398
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
399
-
400
- for file_path, content in files.items():
401
- if not os.path.exists(file_path):
402
- with open(file_path, 'w') as f:
403
- f.write(content)
404
-
405
- logger.info(Message.HEXAGONAL_SERVICE_CREATED_MSG)
406
-
407
- @cli.command()
408
- @click.argument('port')
409
- def start(port: int):
410
- """Start the FastAPI server.."""
411
- try:
412
- subprocess.run(['uvicorn', 'app:app', '--port', str(port), '--reload'], check=True)
413
- except subprocess.CalledProcessError as e:
414
- logger.error(f'{Message.SERVER_NETWORK_ACCESS_ERROR_MSG}: {e}')
415
-
416
-
417
- @cli.command()
418
- @click.argument('port')
419
- def serve(port: int):
420
- """Set up the FastAPI server accessible from any machine."""
421
- try:
422
- subprocess.run(['uvicorn', 'app:app', '--host', '0.0.0.0', '--port', str(port), '--reload'], check=True)
423
- except subprocess.CalledProcessError as e:
424
- logger.error(f'{Message.SERVER_NETWORK_ACCESS_ERROR_MSG}: {e}')
425
-
426
-
427
- @cli.command("clean-redis")
428
- @click.argument('redis_env')
429
- async def clean_redis(redis_env: str):
430
- try:
431
- from Osdental.RedisCache.Redis import RedisCacheAsync
432
- redis_url = os.getenv(redis_env)
433
- if not redis_url:
434
- logger.warning(f'Environment variable not found: {redis_env}')
435
- return
436
-
437
- redis = RedisCacheAsync(redis_url=redis_url)
438
- await redis.flush()
439
- logger.info(Message.REDIS_CLEANUP_SUCCESS_MSG)
440
- except Exception as e:
441
- logger.error(f'{Message.REDIS_CLEANUP_ERROR_MSG}: {e}')
442
-
443
-
444
- @cli.command(name='proto-files')
445
- @click.argument('name')
446
- def proto_files(name: str):
447
- proto_dir = os.path.join('src', 'Infrastructure', 'Grpc', 'Proto').replace('\\', '/')
448
- gen_dir = os.path.join('src', 'Infrastructure', 'Grpc', 'Generated').replace('\\', '/')
449
-
450
- proto_path = os.path.join(proto_dir, f'{name}.proto').replace('\\', '/')
451
- common_path = os.path.join(proto_dir, 'Common.proto').replace('\\', '/')
452
-
453
- common_py = os.path.join(gen_dir, 'Common_pb2.py').replace('\\', '/')
454
- common_grpc_py = os.path.join(gen_dir, 'Common_pb2_grpc.py').replace('\\', '/')
455
-
456
- proto_files = [proto_path]
457
- if not (os.path.exists(common_py) and os.path.exists(common_grpc_py)):
458
- proto_files.append(common_path)
459
-
460
- cmd = [
461
- sys.executable, "-m", "grpc_tools.protoc",
462
- f"-I={proto_dir}",
463
- f"--python_out={gen_dir}",
464
- f"--grpc_python_out={gen_dir}",
465
- *proto_files
466
- ]
467
-
468
- subprocess.run(cmd, shell=False, check=True)
469
- logger.info(Message.PROTO_FILES_GENERATED_MSG)
470
-
471
-
472
-
473
- @cli.command('run-server')
474
- @click.option('--host', default="0.0.0.0", help='Host where to set up the server')
475
- @click.option('--port', default=5000, help='Server port')
476
- @click.option('--workers', default=4, help='Number of workers')
477
- def run_server(host, port, workers):
478
- """
479
- Launch the application with uvicorn on Windows or gunicorn on other systems
480
- :host
481
- :port
482
- :workers
483
- """
484
- if sys.platform.startswith('win'):
485
- cmd = [
486
- sys.executable, '-m', 'uvicorn',
487
- 'app:app',
488
- '--host', host,
489
- '--port', str(port),
490
- '--workers', str(workers)
491
- ]
492
- else:
493
- cmd = [
494
- sys.executable, '-m', 'gunicorn',
495
- 'app:app',
496
- '-k', 'uvicorn.workers.UvicornWorker',
497
- '--bind', f'{host}:{port}',
498
- '--workers', str(workers)
499
- ]
500
-
501
- logger.info(f'Running: {' '.join(cmd)}')
502
- subprocess.run(cmd)
503
-
504
- if __name__ == "__main__":
505
- cli()
File without changes
File without changes