fmconsult-utils-python-sdk 2.0.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.
- __init__.py +0 -0
- fmconsult/__init__.py +0 -0
- fmconsult/auth/__init__.py +0 -0
- fmconsult/auth/decorator.py +37 -0
- fmconsult/auth/profile_builder.py +12 -0
- fmconsult/auth/token_manager.py +41 -0
- fmconsult/database/__init__.py +0 -0
- fmconsult/database/connection.py +34 -0
- fmconsult/database/models/__init__.py +0 -0
- fmconsult/database/models/application.py +26 -0
- fmconsult/database/models/base.py +25 -0
- fmconsult/database/models/city.py +27 -0
- fmconsult/database/models/country.py +23 -0
- fmconsult/database/models/state.py +25 -0
- fmconsult/decorators/__init__.py +0 -0
- fmconsult/decorators/auth.py +116 -0
- fmconsult/exceptions/__init__.py +0 -0
- fmconsult/exceptions/authorization.py +5 -0
- fmconsult/exceptions/bad_request_exception.py +7 -0
- fmconsult/exceptions/conflict_exception.py +7 -0
- fmconsult/exceptions/forbidden_exception.py +7 -0
- fmconsult/exceptions/not_found_exception.py +7 -0
- fmconsult/exceptions/not_implemented_exception.py +7 -0
- fmconsult/exceptions/unauthorized_exception.py +6 -0
- fmconsult/exceptions/unavailable_exception.py +7 -0
- fmconsult/http/__init__.py +0 -0
- fmconsult/http/api.py +108 -0
- fmconsult/utils/__init__.py +0 -0
- fmconsult/utils/cast.py +21 -0
- fmconsult/utils/cnpj.py +29 -0
- fmconsult/utils/configs.py +11 -0
- fmconsult/utils/cpf.py +27 -0
- fmconsult/utils/date.py +46 -0
- fmconsult/utils/enum.py +9 -0
- fmconsult/utils/haversine.py +34 -0
- fmconsult/utils/object.py +6 -0
- fmconsult/utils/pagination.py +19 -0
- fmconsult/utils/string.py +58 -0
- fmconsult/utils/url.py +30 -0
- fmconsult_utils_python_sdk-2.0.0.dist-info/METADATA +21 -0
- fmconsult_utils_python_sdk-2.0.0.dist-info/RECORD +44 -0
- fmconsult_utils_python_sdk-2.0.0.dist-info/WHEEL +5 -0
- fmconsult_utils_python_sdk-2.0.0.dist-info/licenses/LICENSE +5 -0
- fmconsult_utils_python_sdk-2.0.0.dist-info/top_level.txt +2 -0
__init__.py
ADDED
|
File without changes
|
fmconsult/__init__.py
ADDED
|
File without changes
|
|
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)
|
|
File without changes
|
|
@@ -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)
|
|
File without changes
|
|
@@ -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)
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
fmconsult/http/api.py
ADDED
|
@@ -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
|
|
File without changes
|
fmconsult/utils/cast.py
ADDED
|
@@ -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")
|
fmconsult/utils/cnpj.py
ADDED
|
@@ -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)
|
fmconsult/utils/cpf.py
ADDED
|
@@ -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)
|
fmconsult/utils/date.py
ADDED
|
@@ -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
|
fmconsult/utils/enum.py
ADDED
|
@@ -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,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
|
fmconsult/utils/url.py
ADDED
|
@@ -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,44 @@
|
|
|
1
|
+
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
fmconsult/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
fmconsult/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
fmconsult/auth/decorator.py,sha256=gwKeN0BP9uWbFJ31g8HckTp8gQBJHFAiv5WsPdBd6JI,1601
|
|
5
|
+
fmconsult/auth/profile_builder.py,sha256=fpKzRGHUft2VqVq1qHc9aOGdika5ZUwp_WHTVRaqD0o,367
|
|
6
|
+
fmconsult/auth/token_manager.py,sha256=9sn-ZFPxeF9_SGwI3jHWQ_auqmYxWgABQ-Mv11JQo4Y,1710
|
|
7
|
+
fmconsult/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
fmconsult/database/connection.py,sha256=qCvSWHrv_2xLauKwTcmN-Sux0yOnwSZ0Hj4WjuCmFp4,1637
|
|
9
|
+
fmconsult/database/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
fmconsult/database/models/application.py,sha256=G1ltnMMitKc0RutEZK2RkW6fQr3IrHjeG26o321jyrU,572
|
|
11
|
+
fmconsult/database/models/base.py,sha256=_V-Q8kFIX1GGNnEYcHfoLnkIeknPKhtTW2edc_LcZkw,875
|
|
12
|
+
fmconsult/database/models/city.py,sha256=S5IzSMzuEqabLHeWlr-4iZf1RS73kiRtTii-qwmU5FU,985
|
|
13
|
+
fmconsult/database/models/country.py,sha256=SR3eD3AwR263EvofKnxvLaMMFcIWtu4ZrKk0ZWMG1U0,837
|
|
14
|
+
fmconsult/database/models/state.py,sha256=EI2OAEZbCh41wkRkY_UHXjAo62EBzqCImuokULuXgAE,912
|
|
15
|
+
fmconsult/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
fmconsult/decorators/auth.py,sha256=ad_D5_MeTE04TUS_ttD1v1d4br_pVvGOBu4UvzrVjds,4428
|
|
17
|
+
fmconsult/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
fmconsult/exceptions/authorization.py,sha256=u1ZAnEsiTtBA3HODYwRq7yTq3eaHHrmMhx09rQrBbKU,138
|
|
19
|
+
fmconsult/exceptions/bad_request_exception.py,sha256=EPEuRRl38Cujm3XSy1kAghji_WzOjqdwncBTWJskzCQ,265
|
|
20
|
+
fmconsult/exceptions/conflict_exception.py,sha256=UroQfYRy4kNDRzd_S1lZk9D51HpKdS-ubemSvnEBiEs,265
|
|
21
|
+
fmconsult/exceptions/forbidden_exception.py,sha256=End5OlVPXJjn9LsSxkIpsgfwmO1GibCNDDo8AiGGIGM,273
|
|
22
|
+
fmconsult/exceptions/not_found_exception.py,sha256=AkOljUsBY4gOWjxNQFHr5LY3PKiNH9OwDb0ybr5YEmE,244
|
|
23
|
+
fmconsult/exceptions/not_implemented_exception.py,sha256=c4X7j_fJrj-vGFYvgtBHBHGhqxAQc2dN2R0rFZW23Vo,259
|
|
24
|
+
fmconsult/exceptions/unauthorized_exception.py,sha256=yYAcrB_krgTMw8Qpq3fjG4AkWk-keZCxQ_jVWE815i4,232
|
|
25
|
+
fmconsult/exceptions/unavailable_exception.py,sha256=IJMPr8B9cb4csvKPGEBP0B06w2_xDKCzUyBb6e2BgkI,247
|
|
26
|
+
fmconsult/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
fmconsult/http/api.py,sha256=_Q5TvWbAHEbE9yLo4G5_1duUsAlff67HxpoDCpuMQFQ,3961
|
|
28
|
+
fmconsult/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
fmconsult/utils/cast.py,sha256=DF0dsghqlGVO_5QkHzcxMZ7ZrRCTN3oYOFIaRXL_Hi4,517
|
|
30
|
+
fmconsult/utils/cnpj.py,sha256=2b4WMQh-rBrGWdHk_Q1PjRqQ51_71efttBF2_IPmf5E,503
|
|
31
|
+
fmconsult/utils/configs.py,sha256=69cNAuCa3GgmoI7prc4-jENmw8un84ll6CTqMHjboIg,302
|
|
32
|
+
fmconsult/utils/cpf.py,sha256=HtW8D2nEyFS8OSNJAf_T6DJ_lEreN7z5sIi0cd59FUw,647
|
|
33
|
+
fmconsult/utils/date.py,sha256=wRBxYUJx5kzvKGYFNDbGAGW5SNKV34WdM13mwy78tyE,1335
|
|
34
|
+
fmconsult/utils/enum.py,sha256=K7WjriWIx-iRzjDH3snFwPrAoVcZ7DJaXNmPUPoA4jE,235
|
|
35
|
+
fmconsult/utils/haversine.py,sha256=0RlGI10MJDJP4mLVFcX9Q3wW_iIc6FDfyCbvl8D-RsQ,1142
|
|
36
|
+
fmconsult/utils/object.py,sha256=1i0SS36TDQOZTvNvBI8wZ1ZJ2ug0l5Mw4GeKkcWqp2k,126
|
|
37
|
+
fmconsult/utils/pagination.py,sha256=LHUhyMuzyCkKBOcmPLW1O4sZ6Mv2v6d7qMpRBqZSzeI,766
|
|
38
|
+
fmconsult/utils/string.py,sha256=3GvU2T_2kTd9uZR-nNr-L22RKUlJ3BytpINZmuuFg6I,1830
|
|
39
|
+
fmconsult/utils/url.py,sha256=Mz2_Af2PAZHRazqgVjkK3Odm5pQwgBQwivP1ZDgbFXU,682
|
|
40
|
+
fmconsult_utils_python_sdk-2.0.0.dist-info/licenses/LICENSE,sha256=YZjQnFjzXGFa0D3iCQkNDwGBNuTGJREkRmItGX1B3YM,122
|
|
41
|
+
fmconsult_utils_python_sdk-2.0.0.dist-info/METADATA,sha256=JJNRcxF8VbHWiXAJvDMURPDM3UN4DvzDNo2x8RlmUQ4,760
|
|
42
|
+
fmconsult_utils_python_sdk-2.0.0.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
|
43
|
+
fmconsult_utils_python_sdk-2.0.0.dist-info/top_level.txt,sha256=q7f6ezwPmpsLq0feZx3vFnNAMRFM88R5IMcMK0YIyUo,19
|
|
44
|
+
fmconsult_utils_python_sdk-2.0.0.dist-info/RECORD,,
|