creditpulse 0.0.1__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.
- creditpulse-0.0.1/LICENSE +21 -0
- creditpulse-0.0.1/PKG-INFO +13 -0
- creditpulse-0.0.1/README.md +0 -0
- creditpulse-0.0.1/creditpulse/__init__.py +25 -0
- creditpulse-0.0.1/creditpulse/bases_externas/__init__.py +0 -0
- creditpulse-0.0.1/creditpulse/bases_externas/database.py +39 -0
- creditpulse-0.0.1/creditpulse/bases_externas/external.py +43 -0
- creditpulse-0.0.1/creditpulse/bases_externas/schema.py +80 -0
- creditpulse-0.0.1/creditpulse/bases_externas/truora.py +194 -0
- creditpulse-0.0.1/creditpulse/check.py +27 -0
- creditpulse-0.0.1/creditpulse/cli/__init__.py +0 -0
- creditpulse-0.0.1/creditpulse/cli/commands.py +11 -0
- creditpulse-0.0.1/creditpulse/common/__init__.py +0 -0
- creditpulse-0.0.1/creditpulse/common/error_messages.py +39 -0
- creditpulse-0.0.1/creditpulse/requests/__init__.py +0 -0
- creditpulse-0.0.1/creditpulse/requests/request_manager.py +114 -0
- creditpulse-0.0.1/creditpulse.egg-info/PKG-INFO +13 -0
- creditpulse-0.0.1/creditpulse.egg-info/SOURCES.txt +24 -0
- creditpulse-0.0.1/creditpulse.egg-info/dependency_links.txt +1 -0
- creditpulse-0.0.1/creditpulse.egg-info/entry_points.txt +2 -0
- creditpulse-0.0.1/creditpulse.egg-info/requires.txt +4 -0
- creditpulse-0.0.1/creditpulse.egg-info/top_level.txt +1 -0
- creditpulse-0.0.1/pyproject.toml +21 -0
- creditpulse-0.0.1/setup.cfg +4 -0
- creditpulse-0.0.1/test/test_back_off.py +56 -0
- creditpulse-0.0.1/test/test_truora_check.py +16 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 svelezsaman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: creditpulse
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A pyhton library for financial analysis with external databases
|
|
5
|
+
Author-email: Santiago Velez <s.velez@saman-wm.com>
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: click==8.1.8
|
|
10
|
+
Requires-Dist: requests==2.32.4
|
|
11
|
+
Requires-Dist: pydantic==2.8.0
|
|
12
|
+
Requires-Dist: pylint==3.3.7
|
|
13
|
+
Dynamic: license-file
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""A clean Python project template with public API.
|
|
2
|
+
|
|
3
|
+
This package provides a simple, clean interface for users to import and use.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
>>> from CreditPulse import Check
|
|
7
|
+
>>> client = Check()
|
|
8
|
+
>>> result_juridica = client.persona_juridica()
|
|
9
|
+
>>> result_natural = client.persona_natural()
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Import version
|
|
13
|
+
try:
|
|
14
|
+
from ._version import version as __version__
|
|
15
|
+
except ImportError:
|
|
16
|
+
__version__ = "unknown"
|
|
17
|
+
|
|
18
|
+
# Package metadata
|
|
19
|
+
__author__ = "Santiago Velez"
|
|
20
|
+
__email__ = "s.velez@saman-wm.com"
|
|
21
|
+
__description__ = "A python library for financial analysis"
|
|
22
|
+
|
|
23
|
+
from .check import Check
|
|
24
|
+
|
|
25
|
+
__all__ = ["Check"]
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clase abstracta para definir una base de datos
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from creditpulse.bases_externas.schema import PersonType, CountryCode, GeneralDatabase
|
|
6
|
+
from creditpulse.requests.request_manager import RequestActionManager
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Database(RequestActionManager, ABC):
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def create_check(self, identificacion: str,
|
|
14
|
+
person_type: PersonType,
|
|
15
|
+
autorizacion_datos: bool = False,
|
|
16
|
+
pais: CountryCode = CountryCode.COLOMBIA
|
|
17
|
+
) -> GeneralDatabase:
|
|
18
|
+
"""
|
|
19
|
+
Implementacion de
|
|
20
|
+
:param identificacion:
|
|
21
|
+
:param person_type:
|
|
22
|
+
:param autorizacion_datos:
|
|
23
|
+
:param pais:
|
|
24
|
+
:return:
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def get_name(self) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Da el nombre de la actual base de datos
|
|
31
|
+
:return:
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def to_general(self) -> GeneralDatabase:
|
|
36
|
+
"""
|
|
37
|
+
Traduce base de datos a general
|
|
38
|
+
:return:
|
|
39
|
+
"""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
External Database Schema standarization
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from creditpulse.bases_externas.database import Database
|
|
10
|
+
from creditpulse.bases_externas.truora import Truora
|
|
11
|
+
from creditpulse.bases_externas.schema import CountryCode, PersonType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BasesDeDatos(str, Enum):
|
|
15
|
+
"""Supported country codes for checks"""
|
|
16
|
+
TRUORA = "tr" # International Lists
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExternalDatabases:
|
|
20
|
+
|
|
21
|
+
def __init__(self, *bases: BasesDeDatos):
|
|
22
|
+
|
|
23
|
+
if len(bases) == 0:
|
|
24
|
+
raise Exception('Debe seleccionar al menos una base de datos')
|
|
25
|
+
|
|
26
|
+
self.data_bases: [Database] = []
|
|
27
|
+
|
|
28
|
+
for base in bases:
|
|
29
|
+
if base == BasesDeDatos.TRUORA:
|
|
30
|
+
self.data_bases.append(Truora())
|
|
31
|
+
|
|
32
|
+
def obtain_data(self, identificacion: str, natural: bool = True):
|
|
33
|
+
response = {}
|
|
34
|
+
|
|
35
|
+
for base in self.data_bases:
|
|
36
|
+
response[base.get_name()] = base.create_check(
|
|
37
|
+
identificacion=identificacion,
|
|
38
|
+
person_type=PersonType.PERSONA if natural else PersonType.COMPANIA,
|
|
39
|
+
pais=CountryCode.COLOMBIA,
|
|
40
|
+
autorizacion_datos=True
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return response
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Definicion de tipos de datos para base de datos externas
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Literal
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CountryCode(str, Enum):
|
|
13
|
+
"""Supported country codes for checks"""
|
|
14
|
+
ALL = "ALL" # International Lists
|
|
15
|
+
BRAZIL = "BR" # Brazil
|
|
16
|
+
COLOMBIA = "CO" # Colombia
|
|
17
|
+
CHILE = "CL" # Chile
|
|
18
|
+
COSTA_RICA = "CR" # Costa Rica
|
|
19
|
+
MEXICO = "MX" # Mexico
|
|
20
|
+
PERU = "PE" # Peru
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PersonType(str, Enum):
|
|
24
|
+
"""Supported country codes for checks"""
|
|
25
|
+
PERSONA = "person" # Used to perform a background check on a person.
|
|
26
|
+
VEHICULO = "vehicle" # Used to perform a background check on a driver and their vehicle
|
|
27
|
+
COMPANIA = "company" # Used to perform a background check on a company
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CheckStatus(str, Enum):
|
|
31
|
+
"""Status values for checks"""
|
|
32
|
+
NOT_STARTED = "not_started" # The check is enqueued and the data collection has not started yet
|
|
33
|
+
IN_PROGRESS = "in_progress" # Data is being collected but some data sources may have finished already
|
|
34
|
+
DELAYED = "delayed" # One or more data sources are taking a long time to query the data. Most data sources will have already finished
|
|
35
|
+
COMPLETED = "completed" # The check finished and 70% or more of the data sources did not end in error status
|
|
36
|
+
ERROR = "error" # The check finished and more than 30% of the data sources ended in error status
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TruoraCheck(BaseModel):
|
|
40
|
+
"""
|
|
41
|
+
Definicion para Check de truora
|
|
42
|
+
"""
|
|
43
|
+
check_id: str = Field(..., description="Unique identifier for the check")
|
|
44
|
+
country: CountryCode = Field(
|
|
45
|
+
...,
|
|
46
|
+
description="""
|
|
47
|
+
Country code -
|
|
48
|
+
ALL for International Lists,
|
|
49
|
+
BR for Brazil,
|
|
50
|
+
CO for Colombia,
|
|
51
|
+
CL for Chile,
|
|
52
|
+
CR for Costa Rica,
|
|
53
|
+
MX for Mexico,
|
|
54
|
+
PE for Peru
|
|
55
|
+
"""
|
|
56
|
+
)
|
|
57
|
+
creation_date: datetime = Field(..., description="When the check was created")
|
|
58
|
+
name_score: int = Field(..., description="Score for name matching")
|
|
59
|
+
id_score: int = Field(..., description="Score for ID matching")
|
|
60
|
+
score: int = Field(..., description="Overall check score")
|
|
61
|
+
status: CheckStatus = Field(..., description="Current status of the check")
|
|
62
|
+
update_date: datetime = Field(..., description="When the check was last updated")
|
|
63
|
+
billing_hub: str = Field(..., description="Billing hub identifier")
|
|
64
|
+
national_id: str = Field(..., description="National ID number being checked")
|
|
65
|
+
type: Literal["person"] = Field(..., description="Type of check (person, business, etc.)")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TruoraCheckData(BaseModel):
|
|
69
|
+
"""
|
|
70
|
+
Definicion para la respuesta de truora
|
|
71
|
+
"""
|
|
72
|
+
check: TruoraCheck = Field(..., description="Check details")
|
|
73
|
+
details: str = Field(..., description="API endpoint for detailed check information")
|
|
74
|
+
self: str = Field(..., description="API endpoint for this check resource")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class GeneralDatabase(BaseModel):
|
|
78
|
+
score: int = Field(..., description="Overall check score")
|
|
79
|
+
creation_date: datetime = Field(..., description="When the check was created")
|
|
80
|
+
national_id: str = Field(..., description="National ID number being checked")
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request manager ara base de datos externa truora
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from creditpulse.requests.request_manager import RequestManager
|
|
11
|
+
from creditpulse.bases_externas.schema import TruoraCheckData, CountryCode, PersonType, CheckStatus, GeneralDatabase
|
|
12
|
+
from creditpulse.common.error_messages import (
|
|
13
|
+
AutorizacionDatosPersonales,
|
|
14
|
+
TruoraApiKeyRequired,
|
|
15
|
+
TruoraGeneralError
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from creditpulse.bases_externas.database import Database
|
|
19
|
+
|
|
20
|
+
# https://www.postman.com/truora-api-docs/truora-api-docs/collection/iwmyaus/truora-collection
|
|
21
|
+
TRUORA_API_VERSIONS = '/v1'
|
|
22
|
+
TRUORA_BASE_URL = 'https://api.checks.truora.com' + TRUORA_API_VERSIONS
|
|
23
|
+
CHECK_URL = TRUORA_BASE_URL + '/checks'
|
|
24
|
+
CHECK_URL_DETAILS = TRUORA_BASE_URL + '/checks/{}/details'
|
|
25
|
+
CHECK_SETTINGS_URL = ' https://api.checks.truora.com/v1/settings'
|
|
26
|
+
|
|
27
|
+
settings_data = {
|
|
28
|
+
'names_matching_type': 'exact',
|
|
29
|
+
'retries': True,
|
|
30
|
+
'max_duration': '3m'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Truora(Database):
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
Clase principal para consultar base de datos externa truora
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def get_name(self) -> str:
|
|
41
|
+
return 'truora'
|
|
42
|
+
|
|
43
|
+
def _get_check(self, check_id: str):
|
|
44
|
+
return self.session.post(CHECK_URL + "/" + check_id)
|
|
45
|
+
|
|
46
|
+
def on_execute(self) -> requests.Response:
|
|
47
|
+
return self._get_check(self.tcheck.check.check_id)
|
|
48
|
+
|
|
49
|
+
def success_callback(self, response: requests.Response) -> None:
|
|
50
|
+
self.logger.info("Consulta a Base De datos ha sido finalizada")
|
|
51
|
+
response_json = response.json()
|
|
52
|
+
self.tcheck = TruoraCheckData(**response_json)
|
|
53
|
+
self.status = self.tcheck.check.status
|
|
54
|
+
|
|
55
|
+
def error_callback(self, response: requests.Response) -> None:
|
|
56
|
+
response_json = response.json()
|
|
57
|
+
inter_check: TruoraCheckData = TruoraCheckData(**response_json)
|
|
58
|
+
|
|
59
|
+
if inter_check.check.status == CheckStatus.DELAYED:
|
|
60
|
+
self.logger.warning("Consulta base de datos ha sido delayed")
|
|
61
|
+
|
|
62
|
+
self.status = inter_check.check.status
|
|
63
|
+
|
|
64
|
+
def is_request_successful(self, response: requests.Response) -> bool:
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
:param response:
|
|
68
|
+
:return:
|
|
69
|
+
"""
|
|
70
|
+
if response.status_code in [200, 203]:
|
|
71
|
+
response_json = response.json()
|
|
72
|
+
try:
|
|
73
|
+
t_checker: TruoraCheckData = TruoraCheckData(**response_json)
|
|
74
|
+
return t_checker.check.status == CheckStatus.COMPLETED
|
|
75
|
+
except (KeyError, TypeError):
|
|
76
|
+
return False
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def __init__(self):
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
self.api_key = os.environ.get('TRUORA_API_TOKEN')
|
|
85
|
+
|
|
86
|
+
if self.api_key is None:
|
|
87
|
+
raise TruoraApiKeyRequired()
|
|
88
|
+
|
|
89
|
+
self.session = requests.session()
|
|
90
|
+
|
|
91
|
+
self.session.headers.update({
|
|
92
|
+
"Truora-API-Key": self.api_key,
|
|
93
|
+
'Accept': 'application/json'
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
self.request_manager = RequestManager(
|
|
97
|
+
manager=self,
|
|
98
|
+
max_retries=10
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self.tcheck: Optional[TruoraCheckData] = None
|
|
102
|
+
|
|
103
|
+
self.logger = logging.getLogger(__name__)
|
|
104
|
+
|
|
105
|
+
self.status: CheckStatus = CheckStatus.NOT_STARTED
|
|
106
|
+
|
|
107
|
+
def create_check(self,
|
|
108
|
+
identificacion: str,
|
|
109
|
+
person_type: PersonType,
|
|
110
|
+
autorizacion_datos: bool = False,
|
|
111
|
+
pais: CountryCode = CountryCode.COLOMBIA
|
|
112
|
+
):
|
|
113
|
+
"""
|
|
114
|
+
Funcion principal para consulatr base de datos externa tuora
|
|
115
|
+
|
|
116
|
+
:param identificacion:
|
|
117
|
+
:param person_type:
|
|
118
|
+
:param autorizacion_datos:
|
|
119
|
+
:param pais:
|
|
120
|
+
:return:
|
|
121
|
+
"""
|
|
122
|
+
if not autorizacion_datos:
|
|
123
|
+
raise AutorizacionDatosPersonales()
|
|
124
|
+
|
|
125
|
+
settings_response = self.session.post(CHECK_SETTINGS_URL, data=settings_data)
|
|
126
|
+
response_json = settings_response.json()
|
|
127
|
+
|
|
128
|
+
if settings_response.status_code not in [200, 201, 202, 203]:
|
|
129
|
+
self.logger.error(f"Error al crear consulta base de datos externa: {response_json['message']}")
|
|
130
|
+
raise TruoraGeneralError(f"Error al crear consulta base de datos externa: {response_json['message']}")
|
|
131
|
+
|
|
132
|
+
form_data = {
|
|
133
|
+
'national_id': identificacion,
|
|
134
|
+
'country': pais,
|
|
135
|
+
'type': person_type,
|
|
136
|
+
'user_authorized': autorizacion_datos,
|
|
137
|
+
'force_creation': True
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
response = self.session.post(CHECK_URL, data=form_data)
|
|
141
|
+
response_json = response.json()
|
|
142
|
+
|
|
143
|
+
if response.status_code not in [200, 201, 202, 203]:
|
|
144
|
+
self.logger.error(f"Error al crear consulta base de datos externa: {response_json['message']}")
|
|
145
|
+
raise TruoraGeneralError(f"Error al crear consulta base de datos externa: {response_json['message']}")
|
|
146
|
+
|
|
147
|
+
self.tcheck = TruoraCheckData(**response_json)
|
|
148
|
+
|
|
149
|
+
if self.tcheck is None:
|
|
150
|
+
raise TruoraGeneralError('Check de truora no fue creado')
|
|
151
|
+
|
|
152
|
+
self.request_manager.start()
|
|
153
|
+
|
|
154
|
+
open('file.txt', "w+").write(json.dumps(self.get_check_details(self.tcheck.check.check_id)))
|
|
155
|
+
|
|
156
|
+
return self.to_general()
|
|
157
|
+
|
|
158
|
+
def get_check(self, check_id: str) -> TruoraCheckData:
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
:param check_id:
|
|
162
|
+
:return:
|
|
163
|
+
"""
|
|
164
|
+
response = self._get_check(check_id=check_id)
|
|
165
|
+
if response.status_code not in range(200, 203):
|
|
166
|
+
self.logger.error(response.text)
|
|
167
|
+
raise TruoraGeneralError(response.text)
|
|
168
|
+
|
|
169
|
+
response_json = response.json()
|
|
170
|
+
return TruoraCheckData(**response_json)
|
|
171
|
+
|
|
172
|
+
def get_check_details(self, check_id: str):
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
:param check_id:
|
|
176
|
+
:return:
|
|
177
|
+
"""
|
|
178
|
+
response = self.session.post(CHECK_URL_DETAILS.format(check_id))
|
|
179
|
+
if response.status_code not in range(200, 203):
|
|
180
|
+
self.logger.error(response.text)
|
|
181
|
+
raise TruoraGeneralError(response.text)
|
|
182
|
+
|
|
183
|
+
return response.json()
|
|
184
|
+
|
|
185
|
+
def to_general(self) -> GeneralDatabase:
|
|
186
|
+
"""
|
|
187
|
+
Traduce truora a general
|
|
188
|
+
:return:
|
|
189
|
+
"""
|
|
190
|
+
return GeneralDatabase(
|
|
191
|
+
score=self.tcheck.check.score,
|
|
192
|
+
creation_date=self.tcheck.check.creation_date,
|
|
193
|
+
national_id=self.tcheck.check.national_id
|
|
194
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clase pincipal para analysis de creditos financieros
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from creditpulse.bases_externas.external import ExternalDatabases, BasesDeDatos
|
|
8
|
+
from creditpulse.bases_externas.truora import Truora
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Check:
|
|
12
|
+
"""
|
|
13
|
+
Clase que provee interfaz para analysis the personas
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
# Module logger
|
|
18
|
+
self.logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
def get_data(self, identificacion: str, natural: bool = True):
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
:return: Analysis financiero de la persona natural o juridica
|
|
24
|
+
"""
|
|
25
|
+
external = ExternalDatabases(BasesDeDatos.TRUORA)
|
|
26
|
+
|
|
27
|
+
return external.obtain_data(identificacion=identificacion, natural=natural)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manejo de error generales
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
AUTORIZACION_BASE_DE_DATOS = """
|
|
6
|
+
Es necesario obtener autorizacion para el el uso y lectura de base de datos
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
TRUORA_API_KEY = """
|
|
10
|
+
Debe obtener un API key de truora para consulta bases de datos externas, mas info en: https://dev.truora.com/checks/account/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AutorizacionDatosPersonales(Exception):
|
|
15
|
+
"""
|
|
16
|
+
Error por falta de autorizacion para uso de datos personales
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super().__init__(AUTORIZACION_BASE_DE_DATOS)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TruoraApiKeyRequired(Exception):
|
|
24
|
+
"""
|
|
25
|
+
Error por falta de api key de truora
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
super().__init__(TRUORA_API_KEY)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TruoraGeneralError(Exception):
|
|
33
|
+
"""
|
|
34
|
+
Error general de truora que permite dar mensaje de error como paramtero
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, msg: str):
|
|
38
|
+
super().__init__(msg)
|
|
39
|
+
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clase para hacer exponential back of de algun callback
|
|
3
|
+
"""
|
|
4
|
+
import time
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RequestActionManager(ABC):
|
|
12
|
+
"""Abstract base class for handling HTTP requests with decision logic"""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def on_execute(self) -> requests.Response:
|
|
16
|
+
"""Execute the HTTP request and return the response"""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def success_callback(self, response: requests.Response) -> None:
|
|
20
|
+
"""Handle successful response"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def error_callback(self, response: requests.Response) -> None:
|
|
24
|
+
"""Handle error response"""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def is_request_successful(self, response: requests.Response) -> bool:
|
|
28
|
+
"""Determine if the request was successful"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RequestManager:
|
|
32
|
+
"""
|
|
33
|
+
Maneja los request para poder hacer un exponential backoff
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, manager: RequestActionManager, max_retries: int = 10):
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
:param decision: Objeto que ayuda a tomar decisiones para el exponential backoff
|
|
40
|
+
:param max_retries:
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
self.initial_delay = 1.0
|
|
44
|
+
self.max_delay = 60.0
|
|
45
|
+
self.backoff_factor = 1.5
|
|
46
|
+
self.max_retries = max_retries
|
|
47
|
+
self.manager = manager
|
|
48
|
+
|
|
49
|
+
# State variables
|
|
50
|
+
self.current_delay = self.initial_delay
|
|
51
|
+
self.retry_count = 0
|
|
52
|
+
self.running = False
|
|
53
|
+
|
|
54
|
+
def reset_delay(self):
|
|
55
|
+
"""
|
|
56
|
+
Re-inicia el delay
|
|
57
|
+
:return:
|
|
58
|
+
"""
|
|
59
|
+
self.current_delay = self.initial_delay
|
|
60
|
+
self.retry_count = 0
|
|
61
|
+
|
|
62
|
+
def increase_delay(self):
|
|
63
|
+
"""
|
|
64
|
+
Aumenta el delay cada
|
|
65
|
+
:return:
|
|
66
|
+
"""
|
|
67
|
+
self.current_delay = min(self.current_delay * self.backoff_factor, self.max_delay)
|
|
68
|
+
self.retry_count += 1
|
|
69
|
+
|
|
70
|
+
def should_continue(self) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Determina si debe seguir ejecutando la accion
|
|
73
|
+
:return:
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
if not self.running:
|
|
77
|
+
return False
|
|
78
|
+
if self.max_retries is not None and self.retry_count >= self.max_retries:
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def stop(self):
|
|
83
|
+
"""
|
|
84
|
+
Detiene la ejecusion fozosamente
|
|
85
|
+
:return:
|
|
86
|
+
"""
|
|
87
|
+
self.running = False
|
|
88
|
+
|
|
89
|
+
def start(self):
|
|
90
|
+
"""
|
|
91
|
+
Inicia el proceso de llamada con backoof
|
|
92
|
+
:return:
|
|
93
|
+
"""
|
|
94
|
+
self.running = True
|
|
95
|
+
while self.should_continue():
|
|
96
|
+
try:
|
|
97
|
+
response = self.manager.on_execute()
|
|
98
|
+
|
|
99
|
+
if self.manager.is_request_successful(response=response):
|
|
100
|
+
self.stop()
|
|
101
|
+
self.manager.success_callback(response=response)
|
|
102
|
+
self.reset_delay()
|
|
103
|
+
else:
|
|
104
|
+
self.manager.error_callback(response=response)
|
|
105
|
+
self.increase_delay()
|
|
106
|
+
|
|
107
|
+
except requests.exceptions.RequestException:
|
|
108
|
+
self.increase_delay()
|
|
109
|
+
except KeyboardInterrupt:
|
|
110
|
+
self.stop()
|
|
111
|
+
except Exception:
|
|
112
|
+
self.increase_delay()
|
|
113
|
+
if self.should_continue():
|
|
114
|
+
time.sleep(self.current_delay)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: creditpulse
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A pyhton library for financial analysis with external databases
|
|
5
|
+
Author-email: Santiago Velez <s.velez@saman-wm.com>
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: click==8.1.8
|
|
10
|
+
Requires-Dist: requests==2.32.4
|
|
11
|
+
Requires-Dist: pydantic==2.8.0
|
|
12
|
+
Requires-Dist: pylint==3.3.7
|
|
13
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
creditpulse/__init__.py
|
|
5
|
+
creditpulse/check.py
|
|
6
|
+
creditpulse.egg-info/PKG-INFO
|
|
7
|
+
creditpulse.egg-info/SOURCES.txt
|
|
8
|
+
creditpulse.egg-info/dependency_links.txt
|
|
9
|
+
creditpulse.egg-info/entry_points.txt
|
|
10
|
+
creditpulse.egg-info/requires.txt
|
|
11
|
+
creditpulse.egg-info/top_level.txt
|
|
12
|
+
creditpulse/bases_externas/__init__.py
|
|
13
|
+
creditpulse/bases_externas/database.py
|
|
14
|
+
creditpulse/bases_externas/external.py
|
|
15
|
+
creditpulse/bases_externas/schema.py
|
|
16
|
+
creditpulse/bases_externas/truora.py
|
|
17
|
+
creditpulse/cli/__init__.py
|
|
18
|
+
creditpulse/cli/commands.py
|
|
19
|
+
creditpulse/common/__init__.py
|
|
20
|
+
creditpulse/common/error_messages.py
|
|
21
|
+
creditpulse/requests/__init__.py
|
|
22
|
+
creditpulse/requests/request_manager.py
|
|
23
|
+
test/test_back_off.py
|
|
24
|
+
test/test_truora_check.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
creditpulse
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "creditpulse"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "A pyhton library for financial analysis with external databases"
|
|
5
|
+
authors = [{ name = "Santiago Velez", email = "s.velez@saman-wm.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.7"
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
"click==8.1.8",
|
|
11
|
+
"requests==2.32.4",
|
|
12
|
+
"pydantic==2.8.0",
|
|
13
|
+
"pylint==3.3.7"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["setuptools>=61.0"]
|
|
18
|
+
build-backend = "setuptools.build_meta"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
creditpulse = "creditpulse.cli.commands:main"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from creditpulse.requests.request_manager import RequestManager, RequestActionManager
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SimpleCheck(RequestActionManager):
|
|
8
|
+
|
|
9
|
+
def __init__(self, endpoint):
|
|
10
|
+
self.endpoint = endpoint
|
|
11
|
+
self.success_called = False
|
|
12
|
+
self.error_called = False
|
|
13
|
+
|
|
14
|
+
def on_execute(self) -> requests.Response:
|
|
15
|
+
return requests.get(self.endpoint)
|
|
16
|
+
|
|
17
|
+
def success_callback(self, response: requests.Response) -> None:
|
|
18
|
+
self.success_called = True
|
|
19
|
+
|
|
20
|
+
def error_callback(self, response: requests.Response) -> None:
|
|
21
|
+
self.error_called = True
|
|
22
|
+
|
|
23
|
+
def is_request_successful(self, response: requests.Response) -> bool:
|
|
24
|
+
return response.status_code in [200, 201]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestRequestManager(unittest.TestCase):
|
|
28
|
+
|
|
29
|
+
def test_request_success(self):
|
|
30
|
+
check = SimpleCheck(endpoint='https://httpbin.org/get')
|
|
31
|
+
manager = RequestManager(
|
|
32
|
+
manager=check,
|
|
33
|
+
max_retries=1
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
manager.start()
|
|
37
|
+
|
|
38
|
+
self.assertTrue(check.success_called, "Success callback was not called")
|
|
39
|
+
|
|
40
|
+
def test_request_error(self):
|
|
41
|
+
self.error_callback_code = 200
|
|
42
|
+
self.retries = 3
|
|
43
|
+
check = SimpleCheck(endpoint='https://httpbin.org/status/500')
|
|
44
|
+
|
|
45
|
+
manager = RequestManager(
|
|
46
|
+
manager=check,
|
|
47
|
+
max_retries=self.retries,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
manager.start()
|
|
51
|
+
self.assertTrue(check.error_called, "Success callback was not called")
|
|
52
|
+
self.assertEqual(manager.retry_count, self.retries)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == '__main__':
|
|
56
|
+
unittest.main()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from creditpulse.bases_externas.truora import Truora
|
|
4
|
+
from creditpulse.bases_externas.schema import CountryCode, PersonType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestTruoraCheck(unittest.TestCase):
|
|
8
|
+
|
|
9
|
+
def test_simple_request(self):
|
|
10
|
+
client = Truora()
|
|
11
|
+
client.create_check(
|
|
12
|
+
identificacion='1053778047',
|
|
13
|
+
person_type=PersonType.PERSONA,
|
|
14
|
+
pais=CountryCode.COLOMBIA,
|
|
15
|
+
autorizacion_datos=True
|
|
16
|
+
)
|