fmconsult-utils-python-sdk 2.0.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.
Files changed (48) hide show
  1. fmconsult_utils_python_sdk-2.0.0/LICENSE +5 -0
  2. fmconsult_utils_python_sdk-2.0.0/PKG-INFO +21 -0
  3. fmconsult_utils_python_sdk-2.0.0/README.md +0 -0
  4. fmconsult_utils_python_sdk-2.0.0/pyproject.toml +26 -0
  5. fmconsult_utils_python_sdk-2.0.0/setup.cfg +4 -0
  6. fmconsult_utils_python_sdk-2.0.0/src/__init__.py +0 -0
  7. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/__init__.py +0 -0
  8. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/auth/__init__.py +0 -0
  9. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/auth/decorator.py +37 -0
  10. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/auth/profile_builder.py +12 -0
  11. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/auth/token_manager.py +41 -0
  12. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/database/__init__.py +0 -0
  13. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/database/connection.py +34 -0
  14. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/database/models/__init__.py +0 -0
  15. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/database/models/application.py +26 -0
  16. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/database/models/base.py +25 -0
  17. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/database/models/city.py +27 -0
  18. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/database/models/country.py +23 -0
  19. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/database/models/state.py +25 -0
  20. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/decorators/__init__.py +0 -0
  21. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/decorators/auth.py +116 -0
  22. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/exceptions/__init__.py +0 -0
  23. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/exceptions/authorization.py +5 -0
  24. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/exceptions/bad_request_exception.py +7 -0
  25. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/exceptions/conflict_exception.py +7 -0
  26. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/exceptions/forbidden_exception.py +7 -0
  27. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/exceptions/not_found_exception.py +7 -0
  28. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/exceptions/not_implemented_exception.py +7 -0
  29. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/exceptions/unauthorized_exception.py +6 -0
  30. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/exceptions/unavailable_exception.py +7 -0
  31. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/http/__init__.py +0 -0
  32. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/http/api.py +108 -0
  33. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/__init__.py +0 -0
  34. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/cast.py +21 -0
  35. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/cnpj.py +29 -0
  36. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/configs.py +11 -0
  37. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/cpf.py +27 -0
  38. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/date.py +46 -0
  39. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/enum.py +9 -0
  40. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/haversine.py +34 -0
  41. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/object.py +6 -0
  42. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/pagination.py +19 -0
  43. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/string.py +58 -0
  44. fmconsult_utils_python_sdk-2.0.0/src/fmconsult/utils/url.py +30 -0
  45. fmconsult_utils_python_sdk-2.0.0/src/fmconsult_utils_python_sdk.egg-info/PKG-INFO +21 -0
  46. fmconsult_utils_python_sdk-2.0.0/src/fmconsult_utils_python_sdk.egg-info/SOURCES.txt +46 -0
  47. fmconsult_utils_python_sdk-2.0.0/src/fmconsult_utils_python_sdk.egg-info/dependency_links.txt +1 -0
  48. fmconsult_utils_python_sdk-2.0.0/src/fmconsult_utils_python_sdk.egg-info/top_level.txt +2 -0
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Seu Nome
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy...
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: fmconsult-utils-python-sdk
3
+ Version: 2.0.0
4
+ Summary: Canivete suiço da FMConsult
5
+ Author-email: Filipe Coelho <filipe@fmconsult.com.br>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Seu Nome
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy...
11
+
12
+ Project-URL: Homepage, https://github.com/seuuser/minha-biblioteca
13
+ Project-URL: Bug Tracker, https://github.com/seuuser/minha-biblioteca/issues
14
+ Keywords: sdk,api,fmconsult
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
File without changes
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fmconsult-utils-python-sdk"
7
+ version = "2.0.0"
8
+ description = "Canivete suiço da FMConsult"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {file = "LICENSE"}
12
+ authors = [
13
+ {name = "Filipe Coelho", email = "filipe@fmconsult.com.br"},
14
+ ]
15
+ keywords = ["sdk", "api", "fmconsult"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ ]
21
+
22
+ dependencies = []
23
+
24
+ [project.urls]
25
+ "Homepage" = "https://github.com/seuuser/minha-biblioteca"
26
+ "Bug Tracker" = "https://github.com/seuuser/minha-biblioteca/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,37 @@
1
+ import sys, logging, jwt, mongoengine, bottle
2
+ from bottle import request, response
3
+ from .token_manager import TokenManager
4
+ from fmconsult.utils.configs import ConfigPropertiesHelper
5
+ from fmconsult.database.models.application import Application
6
+ from fmconsult.database.connection import DatabaseConnector
7
+
8
+ class AuthDecorator(object):
9
+ def requires_auth(self, f):
10
+ def decorated(*args, **kwargs):
11
+ token = None
12
+ token_manager = TokenManager()
13
+
14
+ try:
15
+ idx_localhost = bottle.request.url.index('127.0.0.1')
16
+ except:
17
+ try:
18
+ token = token_manager.get_jwt_token_from_header()
19
+ except AuthorizationError as reason:
20
+ response.status = 401
21
+ response.headers['Content-Type'] = 'application/json'
22
+ return {'status': reason.code, 'message': reason.description}
23
+
24
+ try:
25
+ token_decoded = token_manager.get_jwt_credetials()
26
+ except jwt.ExpiredSignature:
27
+ response.status = 401
28
+ response.headers['Content-Type'] = 'application/json'
29
+ return {'status': 'ExpiredToken', 'message': 'Token is expired'}
30
+ except jwt.DecodeError as message:
31
+ response.status = 401
32
+ response.headers['Content-Type'] = 'application/json'
33
+ return {'status': 'InvalidToken', 'message': str(message)}
34
+
35
+ return f(*args, **kwargs)
36
+
37
+ return decorated
@@ -0,0 +1,12 @@
1
+ import time
2
+ from fmconsult.utils.configs import ConfigPropertiesHelper
3
+
4
+ class ProfileBuilder(object):
5
+ def __init__(self):
6
+ self.cph = ConfigPropertiesHelper()
7
+
8
+ def build_profile(self, credentials):
9
+ return {
10
+ 'user': credentials,
11
+ 'exp': time.time() + float(self.cph.get_property_value('JWT', 'jwt.expireoffset'))
12
+ }
@@ -0,0 +1,41 @@
1
+ import bottle, jwt
2
+ from bottle import request
3
+ from fmconsult.utils.configs import ConfigPropertiesHelper
4
+ from fmconsult.exceptions.authorization import AuthorizationError
5
+ from .profile_builder import ProfileBuilder
6
+
7
+ class TokenManager(object):
8
+ def __init__(self):
9
+ self.cph = ConfigPropertiesHelper()
10
+
11
+ def get_jwt_token_from_header(self):
12
+ auth = bottle.request.headers.get('Authorization', None)
13
+ if not auth:
14
+ raise AuthorizationError(code='authorization_header_missing', description='Authorization header is expected')
15
+
16
+ parts = auth.split()
17
+
18
+ if parts[0].lower() != 'bearer':
19
+ raise AuthorizationError(code='invalid_header', description='Authorization header must start with Bearer')
20
+ elif len(parts) == 1:
21
+ raise AuthorizationError(code='invalid_header', description='Token not found')
22
+ elif len(parts) > 2:
23
+ raise AuthorizationError(code='invalid_header', description='Authorization header must be Bearer token')
24
+
25
+ return parts[1]
26
+
27
+
28
+ def get_jwt_credetials(self):
29
+ token = self.get_jwt_token_from_header()
30
+ secret_key = self.cph.get_property_value('JWT', 'jwt.secret')
31
+ algorithm = self.cph.get_property_value('JWT', 'jwt.algorithm')
32
+
33
+ return jwt.decode(token, secret_key, algorithms=[algorithm])
34
+
35
+ def get_access_token(self, credentials):
36
+ builder = ProfileBuilder()
37
+ profile = builder.build_profile(credentials)
38
+ secret_key = self.cph.get_property_value('JWT', 'jwt.secret')
39
+ algorithm = self.cph.get_property_value('JWT', 'jwt.algorithm')
40
+
41
+ return jwt.encode(profile, secret_key, algorithm=algorithm)
@@ -0,0 +1,34 @@
1
+ import logging, mongoengine
2
+ from fmconsult.utils.configs import ConfigPropertiesHelper
3
+
4
+ class DatabaseConnector:
5
+ def __init__(self, db_section='MONGODB'):
6
+ self.cph = ConfigPropertiesHelper()
7
+
8
+ self.mongodb_environment = self.cph.get_property_value(db_section, 'mongodb.environment')
9
+ self.mongodb_host = self.cph.get_property_value(db_section, f'mongodb.host.{self.mongodb_environment}')
10
+ self.mongodb_user = self.cph.get_property_value(db_section, 'mongodb.user')
11
+ self.mongodb_pass = self.cph.get_property_value(db_section, 'mongodb.pass')
12
+ self.mongodb_name = self.cph.get_property_value(db_section, 'mongodb.database.name')
13
+
14
+ def connect(self, db_alias='default'):
15
+ try:
16
+ if self.mongodb_host == 'localhost':
17
+ mongoengine.connect(self.mongodb_name, alias=db_alias)
18
+ logging.info(f"successful connection to local database '{self.mongodb_name}'.")
19
+ else:
20
+ dbqs = f'mongodb+srv://{self.mongodb_user}:{self.mongodb_pass}@{self.mongodb_host}/{self.mongodb_name}?retryWrites=true&w=majority'
21
+ mongoengine.connect(host=dbqs, alias=db_alias)
22
+ logging.info(f"successful connection to remote database '{self.mongodb_name}' in '{self.mongodb_host}'")
23
+ except Exception as e:
24
+ error_message = str(e)
25
+ logging.info(f"error connecting to database: {error_message}")
26
+ raise e
27
+
28
+ def disconnect(self, db_alias='default'):
29
+ try:
30
+ mongoengine.disconnect(alias=db_alias)
31
+ logging.info(f"disconnected from mongodb with alias {db_alias}.")
32
+ except Exception as e:
33
+ logging.error('error disconnecting from mongodb.')
34
+ logging.error(e)
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ from mongoengine import *
4
+ from mongoengine.fields import StringField, ListField
5
+ from .base import CustomBaseDocument
6
+
7
+ class Application(CustomBaseDocument):
8
+ meta = {
9
+ 'collection': 'applications',
10
+ 'db_alias': 'security'
11
+ }
12
+
13
+ name = StringField()
14
+ token = StringField()
15
+ client_ids = ListField(default=[])
16
+ api_name = StringField()
17
+
18
+ def to_dict(self):
19
+ json_string = {
20
+ "name": self.name,
21
+ "token": self.token,
22
+ "client_ids": self.client_ids,
23
+ "api_name": self.api_name,
24
+ }
25
+
26
+ return json.dumps(json_string, default=str)
@@ -0,0 +1,25 @@
1
+ from datetime import datetime
2
+ from mongoengine.queryset import QuerySet
3
+ from mongoengine.document import Document
4
+ from mongoengine.fields import BooleanField, DateTimeField
5
+
6
+ class CustomQuerySet(QuerySet):
7
+ def to_json(self):
8
+ return "[%s]" % (",".join([doc.to_json() for doc in self]))
9
+ def to_json_all(self):
10
+ return "[%s]" % (",".join([doc.to_json_all() for doc in self]))
11
+
12
+ class CustomBaseDocument(Document):
13
+ meta = { 'queryset_class': CustomQuerySet, 'abstract': True }
14
+
15
+ created_at = DateTimeField(default=datetime.now)
16
+ updated_at = DateTimeField()
17
+ deleted_at = DateTimeField()
18
+ deleted = BooleanField(default=False)
19
+ active = BooleanField(default=True)
20
+ inactive_at = DateTimeField()
21
+
22
+ def set_all_values(self, data):
23
+ for attr in data:
24
+ if hasattr(self, attr):
25
+ setattr(self, attr, data[attr])
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ from mongoengine import *
4
+ from mongoengine.fields import StringField, EmbeddedDocumentListField
5
+ from .base import CustomBaseDocument
6
+
7
+ class City(CustomBaseDocument):
8
+ meta = { 'collection': 'cities' }
9
+
10
+ name = StringField()
11
+ code = StringField()
12
+ country_code = StringField()
13
+ state_code = StringField()
14
+
15
+ def to_dict(self):
16
+ json_string = {
17
+ 'id': str(self.id),
18
+ 'name': self.name,
19
+ 'code': self.code,
20
+ 'country_code': self.country_code,
21
+ 'state_code': self.state_code,
22
+ 'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
23
+ 'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at is not None else None,
24
+ 'deleted_at': self.deleted_at.strftime("%Y-%m-%d %H:%M:%S") if self.deleted_at is not None else None,
25
+ 'deleted': self.deleted
26
+ }
27
+ return json.dumps(json_string, default=str)
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ from mongoengine import *
4
+ from mongoengine.fields import StringField, EmbeddedDocumentListField
5
+ from .base import CustomBaseDocument
6
+
7
+ class Country(CustomBaseDocument):
8
+ meta = { 'collection': 'countries' }
9
+
10
+ name = StringField()
11
+ code = StringField()
12
+
13
+ def to_dict(self):
14
+ json_string = {
15
+ 'id': str(self.id),
16
+ 'name': self.name,
17
+ 'code': self.code,
18
+ 'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
19
+ 'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at is not None else None,
20
+ 'deleted_at': self.deleted_at.strftime("%Y-%m-%d %H:%M:%S") if self.deleted_at is not None else None,
21
+ 'deleted': self.deleted
22
+ }
23
+ return json.dumps(json_string, default=str)
@@ -0,0 +1,25 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ from mongoengine import *
4
+ from mongoengine.fields import StringField, EmbeddedDocumentListField
5
+ from .base import CustomBaseDocument
6
+
7
+ class State(CustomBaseDocument):
8
+ meta = { 'collection': 'states' }
9
+
10
+ name = StringField()
11
+ code = StringField()
12
+ country_code = StringField()
13
+
14
+ def to_dict(self):
15
+ json_string = {
16
+ 'id': str(self.id),
17
+ 'name': self.name,
18
+ 'code': self.code,
19
+ 'country_code': self.country_code,
20
+ 'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
21
+ 'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at is not None else None,
22
+ 'deleted_at': self.deleted_at.strftime("%Y-%m-%d %H:%M:%S") if self.deleted_at is not None else None,
23
+ 'deleted': self.deleted
24
+ }
25
+ return json.dumps(json_string, default=str)
@@ -0,0 +1,116 @@
1
+ import sys, logging
2
+ import mongoengine
3
+ from bottle import request, response
4
+ from fmconsult.utils.configs import ConfigPropertiesHelper
5
+ from fmconsult.database.models.application import Application
6
+ from fmconsult.database.connection import DatabaseConnector
7
+
8
+ class AuthDecorator(object):
9
+ required_headers = {
10
+ 'x-api-token': {
11
+ 'type': 'str'
12
+ }
13
+ }
14
+
15
+ def __init__(self, additional_required_headers=None):
16
+ if not additional_required_headers is None:
17
+ for header in additional_required_headers:
18
+ self.required_headers[header] = additional_required_headers[header]
19
+
20
+ def require_auth(self, func):
21
+ def decorated(*args, **kwargs):
22
+ db_connector = None
23
+ try:
24
+ logging.info('getting headers from request...')
25
+ for header in self.required_headers:
26
+ value = request.headers.get(header, None)
27
+
28
+ if not value:
29
+ raise Application.DoesNotExist(f"{header} not found in headers")
30
+
31
+ self.required_headers[header]['value'] = value
32
+
33
+ logging.info(self.required_headers)
34
+
35
+ token = self.required_headers['x-api-token']['value']
36
+
37
+ logging.info('iniatlizing database connector...')
38
+ db_connector = DatabaseConnector('API-SECURITY')
39
+ try:
40
+ logging.info('try getting connection to security database...')
41
+ mongoengine.get_connection(alias='security')
42
+ except mongoengine.ConnectionFailure:
43
+ logging.info('failed to connect to security database. creating new connection...')
44
+ db_connector.connect(db_alias='security')
45
+
46
+ cph = ConfigPropertiesHelper()
47
+ api_name = cph.get_property_value('SELF', 'self.api.name')
48
+
49
+ logging.info(f'find exists token: {token} from application: {api_name}')
50
+
51
+ application = None
52
+
53
+ try:
54
+ logging.info('try getting application from database...')
55
+ application = Application.objects.get(token=token, api_name=api_name)
56
+
57
+ except Exception as e:
58
+ try:
59
+ logging.info('try getting connection to security database again...')
60
+ mongoengine.get_connection(alias='security')
61
+ except mongoengine.ConnectionFailure:
62
+ logging.info('failed to connect to security database. creating new connection again...')
63
+ db_connector.connect(db_alias='security')
64
+
65
+ logging.info('try getting application from database again...')
66
+ application = Application.objects.get(token=token, api_name=api_name)
67
+
68
+ if application is None:
69
+ apps = Application.objects.filter()
70
+ logging.info(f'found {len(apps)} applications in database \o/...')
71
+ raise Application.DoesNotExist(f"{token} not found in database or error in connection to database...")
72
+
73
+ for header in self.required_headers:
74
+ if not (header == 'x-api-token'):
75
+ header_type = self.required_headers[header]['type']
76
+ header_field = self.required_headers[header]['field']
77
+ header_value = self.required_headers[header]['value']
78
+
79
+ logging.info(f'find exists header: {header_field} in database')
80
+
81
+ db_field_value = getattr(application, header_field)
82
+
83
+ logging.info(f'validating db value: {db_field_value} with header value: {header_value}')
84
+
85
+ if header_type == 'str':
86
+ if not (str(header_value) == str(db_field_value)):
87
+ raise Application.DoesNotExist(f"{header}:{header_value} does not match to the provided token {token}")
88
+
89
+ if header_type == 'list':
90
+ db_items = set(str(item) for item in db_field_value)
91
+ header_values_list = [str(value.strip()) for value in header_value.split(',')]
92
+
93
+ for header_value in header_values_list:
94
+ if header_value not in db_items:
95
+ raise Application.DoesNotExist(f"{header}:{header_value} does not match any of the provided tokens")
96
+
97
+ logging.info('api protection process successfully!')
98
+ except Application.DoesNotExist as e:
99
+ response.status = 401
100
+ response.headers['Content-Type'] = 'application/json'
101
+ logging.error(e)
102
+ message = 'Error ocurred: {msg} on {line}'.format(msg=str(e), line=sys.exc_info()[-1].tb_lineno)
103
+ logging.error(message)
104
+ return {'status': 'Unauthorized', 'message': str(message)}
105
+
106
+ except Exception as e:
107
+ response.status = 500
108
+ response.headers['Content-Type'] = 'application/json'
109
+ logging.error(e)
110
+ message = 'Error ocurred: {msg} on {line}'.format(msg=str(e), line=sys.exc_info()[-1].tb_lineno)
111
+ logging.error(message)
112
+ return {'status': 'Error', 'message': str(message)}
113
+
114
+ return func(*args, **kwargs)
115
+
116
+ return decorated
@@ -0,0 +1,5 @@
1
+ class AuthorizationError(Exception):
2
+
3
+ def __init__(self, code, description):
4
+ self.code = code
5
+ self.description = description
@@ -0,0 +1,7 @@
1
+ class BadRequestException(Exception):
2
+
3
+ def __init__(self, message="he request is malformed or missing required parameters"):
4
+ self.message = message
5
+ self.status_code = 400
6
+ self.status = 'bad request'
7
+ super().__init__(self.message)
@@ -0,0 +1,7 @@
1
+ class ConflictException(Exception):
2
+
3
+ def __init__(self, message="A conflict occurred with the current state of the resource."):
4
+ self.message = message
5
+ self.status_code = 409
6
+ self.status = 'Conflict'
7
+ super().__init__(self.message)
@@ -0,0 +1,7 @@
1
+ class ForbiddenException(Exception):
2
+
3
+ def __init__(self, message="The server understood the request, but refuses to authorize it."):
4
+ self.message = message
5
+ self.status_code = 403
6
+ self.status = 'forbidden'
7
+ super().__init__(self.message)
@@ -0,0 +1,7 @@
1
+ class NotFoundException(Exception):
2
+
3
+ def __init__(self, message='The requested resource was not found.'):
4
+ self.message = message
5
+ self.status_code = 404
6
+ self.status = 'Not Found'
7
+ super().__init__(self.message)
@@ -0,0 +1,7 @@
1
+ class NotImplementedException(Exception):
2
+
3
+ def __init__(self, message='This functionality is not yet available.'):
4
+ self.message = message
5
+ self.status_code = 501
6
+ self.status = 'Not Implemented'
7
+ super().__init__(self.message)
@@ -0,0 +1,6 @@
1
+ class UnauthorizedException(Exception):
2
+ def __init__(self, message="Unauthorized access"):
3
+ self.message = message
4
+ self.status_code = 401
5
+ self.status = 'unauthorized'
6
+ super().__init__(self.message)
@@ -0,0 +1,7 @@
1
+ class UnavailableException(Exception):
2
+
3
+ def __init__(self, message="Service Temporarily Unavailable"):
4
+ self.message = message
5
+ self.status_code = 503
6
+ self.status = 'unavailable'
7
+ super().__init__(self.message)
@@ -0,0 +1,108 @@
1
+ import logging, jsonpickle, requests
2
+ from enum import Enum
3
+ from http import HTTPMethod
4
+ from requests.exceptions import ConnectionError, ChunkedEncodingError
5
+ from fmconsult.exceptions.bad_request_exception import BadRequestException
6
+ from fmconsult.exceptions.unavailable_exception import UnavailableException
7
+ from fmconsult.exceptions.not_found_exception import NotFoundException
8
+ from fmconsult.exceptions.unauthorized_exception import UnauthorizedException
9
+ from fmconsult.exceptions.conflict_exception import ConflictException
10
+
11
+ class ContentType(Enum):
12
+ APPLICATION_JSON = 'application/json'
13
+ TEXT_PLAIN = 'text/plain'
14
+
15
+ class ApiBase(object):
16
+ def __init__(self, additional_headers=None):
17
+ self.headers = {
18
+ 'x-api-token': self.api_token
19
+ }
20
+
21
+ if not(additional_headers is None):
22
+ for header in additional_headers:
23
+ self.headers[header] = additional_headers[header]
24
+
25
+ def __make_request(self, req_args):
26
+ max_retries = 3
27
+ for attempt in range(max_retries):
28
+ try:
29
+ # Define o timeout, ajustável conforme a necessidade
30
+ res = requests.request(**req_args, timeout=60)
31
+ res.raise_for_status() # Verifica se ocorreu algum erro HTTP
32
+ return res
33
+ except (ConnectionError, ChunkedEncodingError) as e:
34
+ logging.error(f"Connection error occurred: {e}. Retrying {attempt + 1}/{max_retries}...")
35
+ if attempt == max_retries - 1:
36
+ raise # Lança a exceção se o limite de tentativas for atingido
37
+ except requests.exceptions.RequestException as e:
38
+ logging.error(f"An error occurred: {e}")
39
+ raise # Lança qualquer outra exceção que não for de conexão
40
+
41
+ def call_request(self, http_method:HTTPMethod, request_url, params=None, payload=None, content_type:ContentType=ContentType.APPLICATION_JSON):
42
+ try:
43
+ logging.info(f'{str(http_method).upper()} url:')
44
+ logging.info(request_url)
45
+
46
+ self.headers['Content-Type'] = content_type.value
47
+ logging.info(f'{str(http_method).upper()} headers:')
48
+ logging.info(jsonpickle.encode(self.headers))
49
+
50
+ # Verifica se há parâmetros a serem enviados na URL
51
+ if not params is None:
52
+ logging.info(f'{str(http_method).upper()} params:')
53
+ logging.info(jsonpickle.encode(params))
54
+
55
+ # Log do payload (apenas se for POST ou PUT)
56
+ if not payload is None:
57
+ logging.info(f'{str(http_method).upper()} payload:')
58
+ if content_type == ContentType.APPLICATION_JSON:
59
+ logging.info(jsonpickle.encode(payload))
60
+ else:
61
+ logging.info(payload)
62
+
63
+ # Configuração dos parâmetros para requisição
64
+ req_args = {
65
+ 'method': http_method,
66
+ 'url': request_url,
67
+ 'headers': self.headers
68
+ }
69
+
70
+ if params:
71
+ req_args['params'] = params
72
+
73
+ if payload:
74
+ if content_type == ContentType.APPLICATION_JSON:
75
+ req_args['json'] = payload
76
+ else:
77
+ req_args['data'] = payload
78
+
79
+ logging.info(f'request args:')
80
+ logging.info(req_args)
81
+
82
+ res = self.__make_request(req_args)
83
+
84
+ if res.status_code == 503:
85
+ raise UnavailableException()
86
+ elif res.status_code == 404:
87
+ raise NotFoundException(res.content)
88
+ elif res.status_code == 409:
89
+ raise ConflictException(res.content)
90
+ elif res.status_code == 401:
91
+ raise UnauthorizedException(res.content)
92
+ elif res.status_code == 400:
93
+ raise BadRequestException(res.content)
94
+ elif res.status_code != 200:
95
+ raise Exception(res.content)
96
+
97
+ res = res.content.decode('utf-8')
98
+ logging.info(f'{str(http_method).upper()} response:')
99
+ logging.info(jsonpickle.encode(res))
100
+ return res
101
+ except UnavailableException as e:
102
+ raise e
103
+ except BadRequestException as e:
104
+ raise e
105
+ except NotFoundException as e:
106
+ raise e
107
+ except Exception as e:
108
+ raise e
@@ -0,0 +1,21 @@
1
+ from enum import Enum
2
+
3
+ class DataCastUtil:
4
+
5
+ def parse_to_string(self, _object, _field):
6
+ if _field in _object:
7
+ _object[_field] = str(_object[_field])
8
+ return _object
9
+
10
+ def parse_fields_to_string(self, _object, _fields):
11
+ for _field in _fields:
12
+ _object = self.parse_to_string(_object, _field)
13
+ return _object
14
+
15
+ class JSONCastUtil:
16
+
17
+ @staticmethod
18
+ def serialize_enum(obj):
19
+ if isinstance(obj, Enum):
20
+ return obj.value
21
+ raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
@@ -0,0 +1,29 @@
1
+ import re
2
+
3
+ class CNPJUtil:
4
+ def __init__( self ):
5
+ pass
6
+
7
+ def validate(self, cnpj):
8
+ cnpj = ''.join(re.findall('\d', str(cnpj)))
9
+
10
+ if (not cnpj) or (len(cnpj) < 14):
11
+ return False
12
+
13
+ inteiros = map(int, cnpj)
14
+ novo = inteiros[:12]
15
+ prod = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
16
+
17
+ while len(novo) < 14:
18
+ r = sum([x*y for (x, y) in zip(novo, prod)]) % 11
19
+ if r > 1:
20
+ f = 11 - r
21
+ else:
22
+ f = 0
23
+ novo.append(f)
24
+ prod.insert(0, 6)
25
+
26
+ if novo == inteiros:
27
+ return cnpj
28
+
29
+ return False
@@ -0,0 +1,11 @@
1
+ import configparser
2
+
3
+ class ConfigPropertiesHelper(object):
4
+ config = None
5
+
6
+ def __init__(self, config_path='config/configs.ini'):
7
+ self.config = configparser.ConfigParser()
8
+ self.config.read(config_path)
9
+
10
+ def get_property_value(self, section, property):
11
+ return self.config.get(section, property)
@@ -0,0 +1,27 @@
1
+ class CPFUtil:
2
+
3
+ def format( self, cpf ):
4
+ return "%s.%s.%s-%s" % ( cpf[0:3], cpf[3:6], cpf[6:9], cpf[9:11] )
5
+
6
+ def validate(self,cpf):
7
+ cpf_invalidos = [11*str(i) for i in range(10)]
8
+ if cpf in cpf_invalidos:
9
+ return False
10
+ if not cpf.isdigit():
11
+ cpf = cpf.replace(".", "")
12
+ cpf = cpf.replace("-", "")
13
+ if len(cpf) < 11:
14
+ return False
15
+ if len(cpf) > 11:
16
+ return False
17
+ selfcpf = [int(x) for x in cpf]
18
+ cpf = selfcpf[:9]
19
+ while len(cpf) < 11:
20
+ r = sum([(len(cpf)+1-i)*v for i, v in [(x, cpf[x]) for x in range(len(cpf))]]) % 11
21
+ if r > 1:
22
+ f = 11 - r
23
+ else:
24
+ f = 0
25
+ cpf.append(f)
26
+
27
+ return bool(cpf == selfcpf)
@@ -0,0 +1,46 @@
1
+ import re
2
+ from dateutil import parser
3
+ from datetime import datetime, date
4
+
5
+ class DateUtils:
6
+ def is_date(self, string):
7
+ try:
8
+ parser.parse(string)
9
+ return True
10
+ except ValueError:
11
+ return False
12
+
13
+ def calculate_age(self, born):
14
+ today = date.today()
15
+ return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
16
+
17
+ def validate_date_format(date_string):
18
+ return re.match(r'^\d{4}-\d{2}-\d{2}$', date_string) is not None
19
+
20
+ def calculate_hour_difference_from_iso_dates(self, start_date_iso, end_date_iso):
21
+ if start_date_iso is None or end_date_iso is None:
22
+ return 0
23
+
24
+ time_difference = abs(end_date_iso - start_date_iso)
25
+ duration_in_seconds = time_difference.total_seconds()
26
+ duration_in_minutes = duration_in_seconds / 60
27
+
28
+ return duration_in_minutes
29
+
30
+ def convert_minute_in_hours(self, minutes):
31
+ hours = minutes // 60
32
+ minutes_remaining = minutes % 60
33
+
34
+ return f"{int(hours):02d}:{int(minutes_remaining):02d}"
35
+
36
+
37
+ def calculate_hour_difference(self, hour_start_str, hour_end_str):
38
+ if hour_start_str is None or hour_end_str is None:
39
+ return 0
40
+
41
+ hour_start = datetime.strptime(hour_start_str, "%H:%M")
42
+ hour_end = datetime.strptime(hour_end_str, "%H:%M")
43
+ hour_diff = hour_end - hour_start
44
+ hours_in_minutes = (hour_diff.seconds // 60) % 60
45
+
46
+ return hours_in_minutes
@@ -0,0 +1,9 @@
1
+ from enum import Enum
2
+
3
+ class CustomEnum(Enum):
4
+ @classmethod
5
+ def from_value(cls, value):
6
+ for item in cls:
7
+ if item.value == value:
8
+ return item
9
+ raise ValueError(f"{value} is not a valid value for {cls.__name__}")
@@ -0,0 +1,34 @@
1
+ from math import atan2, cos, radians, sin, sqrt
2
+
3
+ class HaversineUtil:
4
+
5
+ @staticmethod
6
+ def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
7
+ """
8
+ Calculate the distance in kilometers between two GPS coordinates using the Haversine formula.
9
+
10
+ Parameters:
11
+ lat1 (float): Latitude of the first point.
12
+ lon1 (float): Longitude of the first point.
13
+ lat2 (float): Latitude of the second point.
14
+ lon2 (float): Longitude of the second point.
15
+
16
+ Returns:
17
+ float: Distance in kilometers between the two points.
18
+ """
19
+ R = 6371.0 # Radius of the Earth in kilometers
20
+
21
+ lat1_rad = radians(lat1)
22
+ lon1_rad = radians(lon1)
23
+ lat2_rad = radians(lat2)
24
+ lon2_rad = radians(lon2)
25
+
26
+ dlon = lon2_rad - lon1_rad
27
+ dlat = lat2_rad - lat1_rad
28
+
29
+ a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2
30
+ c = 2 * atan2(sqrt(a), sqrt(1 - a))
31
+
32
+ distance = R * c
33
+
34
+ return round(distance, 2)
@@ -0,0 +1,6 @@
1
+ from dataclasses import dataclass, asdict
2
+
3
+ @dataclass
4
+ class CustomObject(object):
5
+ def to_dict(self):
6
+ return asdict(self)
@@ -0,0 +1,19 @@
1
+ class PaginationUtil:
2
+ @staticmethod
3
+ def paginate(queryset, page, limit):
4
+ try:
5
+ offset = (page - 1) * limit
6
+ paginated_items = queryset[offset:offset + limit]
7
+ total_items = len(queryset)
8
+
9
+ return {
10
+ 'items': paginated_items,
11
+ 'total': total_items,
12
+ 'page': page,
13
+ 'limit': limit,
14
+ 'pages': (total_items // limit) + (1 if total_items % limit != 0 else 0),
15
+ 'prev': page - 1 if page > 1 else None,
16
+ 'next': page + 1 if page < (total_items // limit) + (1 if total_items % limit != 0 else 0) else None
17
+ }
18
+ except Exception as e:
19
+ raise Exception(f"Pagination error: {str(e)}")
@@ -0,0 +1,58 @@
1
+ class StringUtil(object):
2
+
3
+ def id_generator(self, size=6, chars=string.ascii_uppercase + string.digits):
4
+ return ''.join(random.choice(chars) for _ in range(size))
5
+
6
+ def get_encoded_value(self, value):
7
+ return str(value).decode('cp1252').encode('utf-8')
8
+
9
+ def custom_serializer(self, obj):
10
+ if isinstance(obj, ImportModel):
11
+ return obj.__dict__
12
+ else:
13
+ return None
14
+
15
+ def to_dict(self, input_ordered_dict):
16
+ return loads(dumps(input_ordered_dict, default=self.custom_serializer))
17
+
18
+ def str2bool(self, value):
19
+ return str(value).lower() in ("yes", "true", "t", "1")
20
+
21
+ def isBoolValue(self, value):
22
+ return str(value).lower() in ("yes", "false", "true", "t", "f", "1", "0")
23
+
24
+ def convert_aba_track_to_serial(self, aba_track_code):
25
+ return hex(aba_track_code)
26
+
27
+ def convert_serial_to_wiegand(self, serial_code):
28
+ # separando os bytes
29
+ _p1 = serial_code[-4:]
30
+ _p2 = serial_code[:(len(serial_code)-4)][-4:]
31
+ # convertendo os bytes p/ decimal
32
+ _p1int = int(_p1, 16)
33
+ _p2int = int(_p2, 16)
34
+ # formatando a saida
35
+ _p2str = f'{_p2int:03}'
36
+ wiegand_code = '{b2}-{b1}'.format(b1=_p1int, b2=_p2str)
37
+ return wiegand_code
38
+
39
+ def convert_wiegand_to_serial(self, wiegand_code):
40
+ _wiegand_code = str(wiegand_code).split('-')
41
+
42
+ _b1_hex = hex(int(_wiegand_code[0]))
43
+ _b2_hex = hex(int(_wiegand_code[1]))[-4:]
44
+
45
+ return '{b1}{b2}'.format(b1=_b1_hex, b2=_b2_hex)
46
+
47
+ def convert_decimal_to_wiegand(self, decimal_code):
48
+ return self.convert_serial_to_wiegand(hex(decimal_code))
49
+
50
+ def reverse_decimal_from_hexcode(self, decimal_code):
51
+ hex_code = hex(int(decimal_code)).replace('0x', '')
52
+ p1 = hex_code[0:2]
53
+ p2 = hex_code[2:4]
54
+ p3 = hex_code[4:6]
55
+ p4 = hex_code[6:8]
56
+ inverse_hex_code = '{p4}{p3}{p2}{p1}'.format(p4=p4, p3=p3, p2=p2, p1=p1)
57
+ inverse_dec_code = int(inverse_hex_code, 16)
58
+ return inverse_dec_code
@@ -0,0 +1,30 @@
1
+ from urllib.parse import parse_qs
2
+
3
+ class UrlUtil(object):
4
+ def make_url(self, base_url, paths):
5
+ url = base_url
6
+ for path in paths:
7
+ url = url +'/'+ str(path)
8
+ return url
9
+
10
+ def url_parse(self, query_string):
11
+ url_params = {}
12
+ url_params['params'] = parse_qs(query_string)
13
+
14
+ params = url_params.copy()
15
+
16
+ for key, value in url_params['params'].items():
17
+ if key == 'offset':
18
+ params['offset'] = int(value[0])
19
+ elif key == 'limit':
20
+ params['limit'] = int(value[0])
21
+ else:
22
+ params['params'][key] = value[0]
23
+
24
+ if 'offset' in params['params']:
25
+ del params['params']['offset']
26
+
27
+ if 'limit' in params['params']:
28
+ del params['params']['limit']
29
+
30
+ return params
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: fmconsult-utils-python-sdk
3
+ Version: 2.0.0
4
+ Summary: Canivete suiço da FMConsult
5
+ Author-email: Filipe Coelho <filipe@fmconsult.com.br>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Seu Nome
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy...
11
+
12
+ Project-URL: Homepage, https://github.com/seuuser/minha-biblioteca
13
+ Project-URL: Bug Tracker, https://github.com/seuuser/minha-biblioteca/issues
14
+ Keywords: sdk,api,fmconsult
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
@@ -0,0 +1,46 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/__init__.py
5
+ src/fmconsult/__init__.py
6
+ src/fmconsult/auth/__init__.py
7
+ src/fmconsult/auth/decorator.py
8
+ src/fmconsult/auth/profile_builder.py
9
+ src/fmconsult/auth/token_manager.py
10
+ src/fmconsult/database/__init__.py
11
+ src/fmconsult/database/connection.py
12
+ src/fmconsult/database/models/__init__.py
13
+ src/fmconsult/database/models/application.py
14
+ src/fmconsult/database/models/base.py
15
+ src/fmconsult/database/models/city.py
16
+ src/fmconsult/database/models/country.py
17
+ src/fmconsult/database/models/state.py
18
+ src/fmconsult/decorators/__init__.py
19
+ src/fmconsult/decorators/auth.py
20
+ src/fmconsult/exceptions/__init__.py
21
+ src/fmconsult/exceptions/authorization.py
22
+ src/fmconsult/exceptions/bad_request_exception.py
23
+ src/fmconsult/exceptions/conflict_exception.py
24
+ src/fmconsult/exceptions/forbidden_exception.py
25
+ src/fmconsult/exceptions/not_found_exception.py
26
+ src/fmconsult/exceptions/not_implemented_exception.py
27
+ src/fmconsult/exceptions/unauthorized_exception.py
28
+ src/fmconsult/exceptions/unavailable_exception.py
29
+ src/fmconsult/http/__init__.py
30
+ src/fmconsult/http/api.py
31
+ src/fmconsult/utils/__init__.py
32
+ src/fmconsult/utils/cast.py
33
+ src/fmconsult/utils/cnpj.py
34
+ src/fmconsult/utils/configs.py
35
+ src/fmconsult/utils/cpf.py
36
+ src/fmconsult/utils/date.py
37
+ src/fmconsult/utils/enum.py
38
+ src/fmconsult/utils/haversine.py
39
+ src/fmconsult/utils/object.py
40
+ src/fmconsult/utils/pagination.py
41
+ src/fmconsult/utils/string.py
42
+ src/fmconsult/utils/url.py
43
+ src/fmconsult_utils_python_sdk.egg-info/PKG-INFO
44
+ src/fmconsult_utils_python_sdk.egg-info/SOURCES.txt
45
+ src/fmconsult_utils_python_sdk.egg-info/dependency_links.txt
46
+ src/fmconsult_utils_python_sdk.egg-info/top_level.txt