alegra-e-provider 0.0.1__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.
- alegra/__init__.py +0 -0
- alegra/client.py +141 -0
- alegra/config.py +14 -0
- alegra/models/__init__.py +0 -0
- alegra/models/address.py +8 -0
- alegra/models/company.py +105 -0
- alegra/models/customer.py +17 -0
- alegra/models/dian.py +6 -0
- alegra/models/invoice.py +108 -0
- alegra/models/payroll.py +8 -0
- alegra/models/phone_number.py +12 -0
- alegra/models/test_set.py +8 -0
- alegra/resources/__init__.py +0 -0
- alegra/resources/base.py +133 -0
- alegra/resources/factory.py +36 -0
- alegra_e_provider-0.0.1.dist-info/METADATA +13 -0
- alegra_e_provider-0.0.1.dist-info/RECORD +19 -0
- alegra_e_provider-0.0.1.dist-info/WHEEL +5 -0
- alegra_e_provider-0.0.1.dist-info/top_level.txt +1 -0
alegra/__init__.py
ADDED
|
File without changes
|
alegra/client.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import httpx
|
|
3
|
+
|
|
4
|
+
from alegra.config import ApiConfig
|
|
5
|
+
from alegra.models.company import Company
|
|
6
|
+
from alegra.models.dian import DianResource
|
|
7
|
+
from alegra.models.invoice import Invoice, InvoiceResponse, FileResponse
|
|
8
|
+
from alegra.models.payroll import Payroll
|
|
9
|
+
from alegra.models.test_set import TestSet
|
|
10
|
+
from alegra.resources.factory import ResourceFactory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiClient:
|
|
14
|
+
def __init__(self, config: ApiConfig, async_mode=False):
|
|
15
|
+
self.config = config
|
|
16
|
+
self.base_url = self.config.get_base_url()
|
|
17
|
+
self.async_mode = async_mode
|
|
18
|
+
self._initialize_resources()
|
|
19
|
+
|
|
20
|
+
async def _async_request(self, method, endpoint, **kwargs):
|
|
21
|
+
url = f"{self.base_url}/{endpoint}"
|
|
22
|
+
async with httpx.AsyncClient(
|
|
23
|
+
headers={"Authorization": f"Bearer {self.config.api_key}"}, timeout=30.0
|
|
24
|
+
) as client:
|
|
25
|
+
response = await client.request(method, url, **kwargs)
|
|
26
|
+
return response.json()
|
|
27
|
+
|
|
28
|
+
def _sync_request(self, method, endpoint, **kwargs):
|
|
29
|
+
url = f"{self.base_url}/{endpoint}"
|
|
30
|
+
with requests.Session() as session:
|
|
31
|
+
session.headers.update({"Authorization": f"Bearer {self.config.api_key}"})
|
|
32
|
+
response = session.request(method, url, **kwargs)
|
|
33
|
+
return response.json()
|
|
34
|
+
|
|
35
|
+
def _request(self, method, endpoint, **kwargs):
|
|
36
|
+
if self.async_mode:
|
|
37
|
+
return self._async_request(method, endpoint, **kwargs)
|
|
38
|
+
else:
|
|
39
|
+
return self._sync_request(method, endpoint, **kwargs)
|
|
40
|
+
|
|
41
|
+
def _initialize_resources(self):
|
|
42
|
+
self.company = ResourceFactory(
|
|
43
|
+
self,
|
|
44
|
+
"company",
|
|
45
|
+
self._request,
|
|
46
|
+
{
|
|
47
|
+
"get": {
|
|
48
|
+
"model": Company,
|
|
49
|
+
"response_model": Company,
|
|
50
|
+
"response_key": "company",
|
|
51
|
+
},
|
|
52
|
+
"update": {
|
|
53
|
+
"model": Company,
|
|
54
|
+
"response_model": Company,
|
|
55
|
+
"response_key": "company",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
self.companies = ResourceFactory(
|
|
60
|
+
self,
|
|
61
|
+
"companies",
|
|
62
|
+
self._request,
|
|
63
|
+
{
|
|
64
|
+
"create": {
|
|
65
|
+
"model": Company,
|
|
66
|
+
"response_model": Company,
|
|
67
|
+
"response_key": "company",
|
|
68
|
+
},
|
|
69
|
+
"get": {
|
|
70
|
+
"model": Company,
|
|
71
|
+
"response_model": Company,
|
|
72
|
+
"response_key": "company",
|
|
73
|
+
},
|
|
74
|
+
"update": {
|
|
75
|
+
"model": Company,
|
|
76
|
+
"response_model": Company,
|
|
77
|
+
"response_key": "company",
|
|
78
|
+
},
|
|
79
|
+
"list": {
|
|
80
|
+
"model": Company,
|
|
81
|
+
"response_model": Company,
|
|
82
|
+
"response_key": "companies",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
self.payrolls = ResourceFactory(
|
|
87
|
+
self,
|
|
88
|
+
"payrolls",
|
|
89
|
+
self._request,
|
|
90
|
+
{
|
|
91
|
+
"create": {"model": Payroll, "response_key": "payroll"},
|
|
92
|
+
"get": {"model": Payroll, "response_key": "payroll"},
|
|
93
|
+
"update": {"model": Payroll, "response_key": "payroll"},
|
|
94
|
+
"list": {"model": Payroll, "response_key": "payrolls"},
|
|
95
|
+
"perform__replace": {"model": Payroll, "response_key": "payroll"},
|
|
96
|
+
"perform__cancel": {"model": Payroll, "response_key": "payroll"},
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
self.dian = ResourceFactory(
|
|
100
|
+
self,
|
|
101
|
+
"dian",
|
|
102
|
+
self._request,
|
|
103
|
+
{"list": {"model": DianResource, "response_key": "dian"}},
|
|
104
|
+
)
|
|
105
|
+
self.test_sets = ResourceFactory(
|
|
106
|
+
self,
|
|
107
|
+
"test-sets",
|
|
108
|
+
self._request,
|
|
109
|
+
{
|
|
110
|
+
"create": {"model": TestSet, "response_key": "test_set"},
|
|
111
|
+
"get": {"model": TestSet, "response_key": "test_set"},
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
self.invoices = ResourceFactory(
|
|
115
|
+
self,
|
|
116
|
+
"invoices",
|
|
117
|
+
self._request,
|
|
118
|
+
{
|
|
119
|
+
"create": {
|
|
120
|
+
"model": Invoice,
|
|
121
|
+
"response_model": InvoiceResponse,
|
|
122
|
+
"response_key": "invoice",
|
|
123
|
+
},
|
|
124
|
+
"get": {
|
|
125
|
+
"model": Invoice,
|
|
126
|
+
"response_model": InvoiceResponse,
|
|
127
|
+
"response_key": "invoice",
|
|
128
|
+
},
|
|
129
|
+
"perform__file_xml": {
|
|
130
|
+
"model": FileResponse,
|
|
131
|
+
"endpoint_suffix": "files/XML",
|
|
132
|
+
"response_model": FileResponse,
|
|
133
|
+
"response_key": "file",
|
|
134
|
+
},
|
|
135
|
+
"list": {
|
|
136
|
+
"model": InvoiceResponse,
|
|
137
|
+
"response_model": InvoiceResponse,
|
|
138
|
+
"response_key": "invoices",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
)
|
alegra/config.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ApiConfig(BaseModel):
|
|
5
|
+
api_key: str
|
|
6
|
+
environment: str
|
|
7
|
+
|
|
8
|
+
def get_base_url(self):
|
|
9
|
+
if self.environment == 'sandbox':
|
|
10
|
+
return 'https://sandbox-api.alegra.com/e-provider/col/v1'
|
|
11
|
+
elif self.environment == 'production':
|
|
12
|
+
return 'https://api.alegra.com/e-provider/col/v1'
|
|
13
|
+
else:
|
|
14
|
+
raise ValueError("Invalid environment. Choose 'sandbox' or 'production'.")
|
|
File without changes
|
alegra/models/address.py
ADDED
alegra/models/company.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, EmailStr, field_validator, model_validator
|
|
5
|
+
|
|
6
|
+
from .address import Address
|
|
7
|
+
|
|
8
|
+
IDENTIFICATION_TYPES = [
|
|
9
|
+
"11",
|
|
10
|
+
"12",
|
|
11
|
+
"13",
|
|
12
|
+
"21",
|
|
13
|
+
"22",
|
|
14
|
+
"31",
|
|
15
|
+
"41",
|
|
16
|
+
"42",
|
|
17
|
+
"47",
|
|
18
|
+
"48",
|
|
19
|
+
"50",
|
|
20
|
+
"91",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Webhook(BaseModel):
|
|
25
|
+
url: str = ""
|
|
26
|
+
headers: Dict[str, str] = {}
|
|
27
|
+
status: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Webhooks(BaseModel):
|
|
31
|
+
general: Dict[str, Webhook] = {}
|
|
32
|
+
payrolls: Dict[str, Webhook] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Certificate(BaseModel):
|
|
36
|
+
name: str = ""
|
|
37
|
+
extension: str = ""
|
|
38
|
+
content: str = ""
|
|
39
|
+
password: str = ""
|
|
40
|
+
issuerName: str = ""
|
|
41
|
+
startDate: str = ""
|
|
42
|
+
endDate: str = ""
|
|
43
|
+
serialNumber: str = ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GovernmentStatus(BaseModel):
|
|
47
|
+
payrolls: str = ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NotificationByEmail(BaseModel):
|
|
51
|
+
enabled: bool = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Company(BaseModel):
|
|
55
|
+
id: Optional[str] = None
|
|
56
|
+
name: str
|
|
57
|
+
tradeName: Optional[str] = None
|
|
58
|
+
identification: str
|
|
59
|
+
dv: str
|
|
60
|
+
useAlegraCertificate: bool
|
|
61
|
+
organizationType: int
|
|
62
|
+
identificationType: str
|
|
63
|
+
regimeCode: str = ""
|
|
64
|
+
email: EmailStr
|
|
65
|
+
phone: Optional[str] = ""
|
|
66
|
+
address: Address
|
|
67
|
+
governmentStatus: Optional[GovernmentStatus] = None
|
|
68
|
+
certificate: Optional[Certificate] = None
|
|
69
|
+
webhooks: Optional[Webhooks] = None
|
|
70
|
+
notificationByEmail: Optional[NotificationByEmail] = None
|
|
71
|
+
|
|
72
|
+
@field_validator("dv")
|
|
73
|
+
def validate_dv(cls, v):
|
|
74
|
+
if not re.match(r"^([0-9]{1})$", v):
|
|
75
|
+
raise ValueError("dv must be a single digit")
|
|
76
|
+
return v
|
|
77
|
+
|
|
78
|
+
@field_validator("identificationType")
|
|
79
|
+
def validate_identificationType(cls, v):
|
|
80
|
+
if len(v) > 2:
|
|
81
|
+
raise ValueError("identificationType must have a maximum length of 2")
|
|
82
|
+
if v not in IDENTIFICATION_TYPES:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"identificationType must be one of the allowed values: {', '.join(IDENTIFICATION_TYPES)}"
|
|
85
|
+
)
|
|
86
|
+
return v
|
|
87
|
+
|
|
88
|
+
@field_validator("organizationType")
|
|
89
|
+
def validate_organizationType(cls, v):
|
|
90
|
+
if v not in [1, 2]:
|
|
91
|
+
raise ValueError("organizationType must be 1 or 2")
|
|
92
|
+
return v
|
|
93
|
+
|
|
94
|
+
@model_validator(mode="before")
|
|
95
|
+
def check_certificate(cls, values):
|
|
96
|
+
use_alegra_certificate = values.get("useAlegraCertificate")
|
|
97
|
+
if use_alegra_certificate:
|
|
98
|
+
return values
|
|
99
|
+
|
|
100
|
+
certificate = values.get("certificate")
|
|
101
|
+
if not use_alegra_certificate and not certificate:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"certificate is required when useAlegraCertificate is False"
|
|
104
|
+
)
|
|
105
|
+
return values
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, EmailStr
|
|
4
|
+
|
|
5
|
+
from alegra.models.address import Address
|
|
6
|
+
from alegra.models.phone_number import PhoneNumber
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Customer(BaseModel):
|
|
10
|
+
name: str
|
|
11
|
+
organizationType: int
|
|
12
|
+
identificationType: str
|
|
13
|
+
identificationNumber: Optional[str]
|
|
14
|
+
dv: Optional[str]
|
|
15
|
+
email: Optional[EmailStr] = ""
|
|
16
|
+
address: Optional[Address] = None
|
|
17
|
+
phone: str = ""
|
alegra/models/dian.py
ADDED
alegra/models/invoice.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from alegra.models.customer import Customer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Resolution(BaseModel):
|
|
10
|
+
resolutionNumber: str
|
|
11
|
+
prefix: str
|
|
12
|
+
minNumber: int
|
|
13
|
+
maxNumber: int
|
|
14
|
+
startDate: str
|
|
15
|
+
endDate: str
|
|
16
|
+
technicalKey: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Taxes(BaseModel):
|
|
20
|
+
taxCode: str
|
|
21
|
+
taxAmount: float
|
|
22
|
+
taxPercentage: str
|
|
23
|
+
taxableAmount: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Item(BaseModel):
|
|
27
|
+
code: str
|
|
28
|
+
description: str
|
|
29
|
+
price: float
|
|
30
|
+
quantity: int
|
|
31
|
+
unitCode: str
|
|
32
|
+
note: Optional[str] = ""
|
|
33
|
+
subtotal: float
|
|
34
|
+
taxAmount: float
|
|
35
|
+
total: float
|
|
36
|
+
discount: float
|
|
37
|
+
discountAmount: float
|
|
38
|
+
taxes: List[Taxes] = []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Payment(BaseModel):
|
|
42
|
+
paymentForm: str
|
|
43
|
+
paymentMethod: str
|
|
44
|
+
paymentDueDate: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DiscountOrCharge(BaseModel):
|
|
48
|
+
isCharge: bool
|
|
49
|
+
reason: str
|
|
50
|
+
percentageAmount: float
|
|
51
|
+
amount: float
|
|
52
|
+
baseAmount: float
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TotalAmounts(BaseModel):
|
|
56
|
+
grossTotal: float
|
|
57
|
+
taxableTotal: float
|
|
58
|
+
taxTotal: float
|
|
59
|
+
discountTotal: float
|
|
60
|
+
chargeTotal: float
|
|
61
|
+
advanceTotal: float
|
|
62
|
+
payableTotal: float
|
|
63
|
+
currencyCode: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CompanyID(BaseModel):
|
|
67
|
+
id: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Invoice(BaseModel):
|
|
71
|
+
documentType: str
|
|
72
|
+
resolution: Resolution
|
|
73
|
+
company: CompanyID
|
|
74
|
+
customer: Customer
|
|
75
|
+
number: int
|
|
76
|
+
note: Optional[str]
|
|
77
|
+
items: List[Item]
|
|
78
|
+
payments: List[Payment]
|
|
79
|
+
discountsAndCharges: List[DiscountOrCharge] = []
|
|
80
|
+
totalAmounts: TotalAmounts
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class GovernmentResponse(BaseModel):
|
|
84
|
+
code: str
|
|
85
|
+
message: str
|
|
86
|
+
errorMessages: List[str]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class InvoiceResponse(BaseModel):
|
|
90
|
+
id: str
|
|
91
|
+
companyIdentification: str
|
|
92
|
+
customerIdentification: str
|
|
93
|
+
type: str
|
|
94
|
+
cufe: str
|
|
95
|
+
date: datetime
|
|
96
|
+
prefix: str
|
|
97
|
+
number: int
|
|
98
|
+
fullNumber: str
|
|
99
|
+
status: str
|
|
100
|
+
legalStatus: str
|
|
101
|
+
governmentResponse: GovernmentResponse
|
|
102
|
+
xmlFileName: str
|
|
103
|
+
zipFileName: str
|
|
104
|
+
qrCodeContent: str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class FileResponse(BaseModel):
|
|
108
|
+
content: str
|
alegra/models/payroll.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PhoneNumber(BaseModel):
|
|
7
|
+
country_code: int
|
|
8
|
+
national_number: int
|
|
9
|
+
extension: Optional[str] = None
|
|
10
|
+
italian_leading_zero: Optional[bool] = None
|
|
11
|
+
number_of_leading_zeros: Optional[int] = None
|
|
12
|
+
preferred_domestic_carrier_code: Optional[int] = None
|
|
File without changes
|
alegra/resources/base.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from typing import Callable, Dict
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ApiResource:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
client,
|
|
10
|
+
endpoint: str,
|
|
11
|
+
request_method: Callable,
|
|
12
|
+
actions_config: Dict[str, Dict[str, str]],
|
|
13
|
+
):
|
|
14
|
+
self.client = client
|
|
15
|
+
self.endpoint = endpoint
|
|
16
|
+
self.request_method = request_method
|
|
17
|
+
self.actions_config = actions_config
|
|
18
|
+
|
|
19
|
+
def _is_action_allowed(self, action: str):
|
|
20
|
+
return action in self.actions_config
|
|
21
|
+
|
|
22
|
+
def _parse_response(self, response, action: str):
|
|
23
|
+
response_key = self.actions_config[action].get("response_key")
|
|
24
|
+
if response_key:
|
|
25
|
+
if not response_key in response:
|
|
26
|
+
if response.get("message"):
|
|
27
|
+
raise ValueError(response.get("message"))
|
|
28
|
+
if response.get("errors"):
|
|
29
|
+
raise ValueError(response.get("errors"))
|
|
30
|
+
raise ValueError(f"Response key '{response_key}' not found in response")
|
|
31
|
+
response_data = response.get(response_key, response)
|
|
32
|
+
else:
|
|
33
|
+
response_data = response
|
|
34
|
+
model = self.actions_config[action]["response_model"]
|
|
35
|
+
return model.model_validate(response_data)
|
|
36
|
+
|
|
37
|
+
def _prepare_data(self, data: BaseModel):
|
|
38
|
+
data = data.model_dump()
|
|
39
|
+
if "customer" in data:
|
|
40
|
+
customer_data = data["customer"]
|
|
41
|
+
if customer_data["dv"] is None:
|
|
42
|
+
del customer_data["dv"]
|
|
43
|
+
return {k: v for k, v in data.items() if v is not None}
|
|
44
|
+
|
|
45
|
+
def get(self, resource_id: str):
|
|
46
|
+
action = "get"
|
|
47
|
+
if not self._is_action_allowed(action):
|
|
48
|
+
raise NotImplementedError(
|
|
49
|
+
f"The action 'get' is not allowed for {self.endpoint}"
|
|
50
|
+
)
|
|
51
|
+
endpoint = f"{self.endpoint}/{resource_id}"
|
|
52
|
+
response = self.request_method("GET", endpoint)
|
|
53
|
+
return self._parse_response(response, action)
|
|
54
|
+
|
|
55
|
+
def create(self, data: BaseModel):
|
|
56
|
+
if self.client.async_mode:
|
|
57
|
+
return self.create_async(data)
|
|
58
|
+
else:
|
|
59
|
+
action = "create"
|
|
60
|
+
if not self._is_action_allowed(action):
|
|
61
|
+
raise NotImplementedError(
|
|
62
|
+
f"The action 'create' is not allowed for {self.endpoint}"
|
|
63
|
+
)
|
|
64
|
+
response = self.request_method(
|
|
65
|
+
"POST", self.endpoint, json=self._prepare_data(data)
|
|
66
|
+
)
|
|
67
|
+
return self._parse_response(response, action)
|
|
68
|
+
|
|
69
|
+
async def create_async(self, data: BaseModel):
|
|
70
|
+
action = "create"
|
|
71
|
+
if not self._is_action_allowed(action):
|
|
72
|
+
raise NotImplementedError(
|
|
73
|
+
f"The action 'create' is not allowed for {self.endpoint}"
|
|
74
|
+
)
|
|
75
|
+
response = await self.request_method(
|
|
76
|
+
"POST", self.endpoint, json=self._prepare_data(data)
|
|
77
|
+
)
|
|
78
|
+
return self._parse_response(response, action)
|
|
79
|
+
|
|
80
|
+
def update(self, resource_id: str, data: BaseModel):
|
|
81
|
+
action = "update"
|
|
82
|
+
if not self._is_action_allowed(action):
|
|
83
|
+
raise NotImplementedError(
|
|
84
|
+
f"The action 'update' is not allowed for {self.endpoint}"
|
|
85
|
+
)
|
|
86
|
+
endpoint = f"{self.endpoint}/{resource_id}"
|
|
87
|
+
response = self.request_method("PATCH", endpoint, json=self._prepare_data(data))
|
|
88
|
+
return self._parse_response(response, action)
|
|
89
|
+
|
|
90
|
+
def delete(self, resource_id: str):
|
|
91
|
+
action = "delete"
|
|
92
|
+
if not self._is_action_allowed(action):
|
|
93
|
+
raise NotImplementedError(
|
|
94
|
+
f"The action 'delete' is not allowed for {self.endpoint}"
|
|
95
|
+
)
|
|
96
|
+
endpoint = f"{self.endpoint}/{resource_id}"
|
|
97
|
+
response = self.request_method("DELETE", endpoint)
|
|
98
|
+
return response.status_code == 204
|
|
99
|
+
|
|
100
|
+
def list(self, params=None):
|
|
101
|
+
action = "list"
|
|
102
|
+
if not self._is_action_allowed(action):
|
|
103
|
+
raise NotImplementedError(
|
|
104
|
+
f"The action 'list' is not allowed for {self.endpoint}"
|
|
105
|
+
)
|
|
106
|
+
response = self.request_method("GET", self.endpoint, params=params)
|
|
107
|
+
return [
|
|
108
|
+
self.actions_config[action]["model"].model_validate(item)
|
|
109
|
+
for item in response.get(
|
|
110
|
+
self.actions_config[action].get("response_key", [])
|
|
111
|
+
)
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
def perform_subaction(
|
|
115
|
+
self, resource_id: str, subaction: str, data: BaseModel = None
|
|
116
|
+
):
|
|
117
|
+
action = f"perform__{subaction}"
|
|
118
|
+
if not self._is_action_allowed(action):
|
|
119
|
+
raise NotImplementedError(
|
|
120
|
+
f"The subaction '{subaction}' is not allowed for {self.endpoint}"
|
|
121
|
+
)
|
|
122
|
+
endpoint_suffix = self.actions_config[action].get("endpoint_suffix", subaction)
|
|
123
|
+
endpoint = f"{self.endpoint}/{resource_id}/{endpoint_suffix}"
|
|
124
|
+
response = self.request_method(
|
|
125
|
+
self._request_method_for_subaction(subaction),
|
|
126
|
+
endpoint,
|
|
127
|
+
json=self._prepare_data(data) if data else {},
|
|
128
|
+
)
|
|
129
|
+
return self._parse_response(response, action)
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def _request_method_for_subaction(subaction: str):
|
|
133
|
+
return "POST" if subaction in ["replace", "cancel"] else "GET"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Callable, Dict
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from .base import ApiResource
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResourceFactory:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
client,
|
|
12
|
+
endpoint: str,
|
|
13
|
+
request_method: Callable,
|
|
14
|
+
actions_config: Dict[str, Dict[str, str]],
|
|
15
|
+
):
|
|
16
|
+
self.resource = ApiResource(client, endpoint, request_method, actions_config)
|
|
17
|
+
|
|
18
|
+
def get(self, resource_id):
|
|
19
|
+
return self.resource.get(resource_id)
|
|
20
|
+
|
|
21
|
+
def create(self, data):
|
|
22
|
+
return self.resource.create(data)
|
|
23
|
+
|
|
24
|
+
def update(self, resource_id, data):
|
|
25
|
+
return self.resource.update(resource_id, data)
|
|
26
|
+
|
|
27
|
+
def delete(self, resource_id):
|
|
28
|
+
return self.resource.delete(resource_id)
|
|
29
|
+
|
|
30
|
+
def list(self, params=None):
|
|
31
|
+
return self.resource.list(params)
|
|
32
|
+
|
|
33
|
+
def perform_subaction(
|
|
34
|
+
self, resource_id: str, subaction: str, data: BaseModel = None
|
|
35
|
+
):
|
|
36
|
+
return self.resource.perform_subaction(resource_id, subaction, data)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: alegra-e-provider
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Alegra E-Provider, Python Wrapper for Alegra Electronic Provider API
|
|
5
|
+
Author: Luis Martinez
|
|
6
|
+
Requires-Python: >=3.6
|
|
7
|
+
Requires-Dist: pydantic[email]==2.8.2
|
|
8
|
+
Requires-Dist: requests==2.32.3
|
|
9
|
+
Requires-Dist: httpx==0.27.2
|
|
10
|
+
Dynamic: author
|
|
11
|
+
Dynamic: requires-dist
|
|
12
|
+
Dynamic: requires-python
|
|
13
|
+
Dynamic: summary
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
alegra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
alegra/client.py,sha256=icMG9yK-pT7ipRtwCqll2MRJO9qqaDCRUgF6bCYaKK4,4958
|
|
3
|
+
alegra/config.py,sha256=0X1vmS2rcDcHPz_uP1QP-ui2Vr5JN2ufoWz4uw9c_-I,450
|
|
4
|
+
alegra/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
alegra/models/address.py,sha256=Ze_XAwawYRjz6D1a-T555i5-u5YrVLH_kzMeOJd-nk0,147
|
|
6
|
+
alegra/models/company.py,sha256=kdlLyfQLyGuK3r3rUZp7zFK5XfiH-35dUDQQPhcYBh8,2607
|
|
7
|
+
alegra/models/customer.py,sha256=X9UvI_zageqdgq_mUp97FtFaMAyhqOOxGY9tzDkB7TI,416
|
|
8
|
+
alegra/models/dian.py,sha256=ZJsxY-w05sWpyjW1VjIPdQdlEQ4Tj2OOwADn4qvjwZU,90
|
|
9
|
+
alegra/models/invoice.py,sha256=tZxWdORR8tYRMVuoyAAwJDUh74kp7KO_5HZT431WGtQ,1992
|
|
10
|
+
alegra/models/payroll.py,sha256=OVNz91R4E-R3jEXKelBc748dQXVw91aq5pcM35KfXIY,135
|
|
11
|
+
alegra/models/phone_number.py,sha256=Bw8n62KFBdVUKUCH8Chg3KExEJJYjr8KcPn4GepY4o8,331
|
|
12
|
+
alegra/models/test_set.py,sha256=hzR_8nBmoVljzybK3u6TZUJGlFJw3AM0rAiczLAASew,126
|
|
13
|
+
alegra/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
alegra/resources/base.py,sha256=e3JDEL8Z-2S_mdg4kIC3U_gyUNgmqsTcnBk42HV5V4U,5135
|
|
15
|
+
alegra/resources/factory.py,sha256=9SkaVj8G5sey0exScusCezZNWxipy7ejrkIIuxm2jRA,962
|
|
16
|
+
alegra_e_provider-0.0.1.dist-info/METADATA,sha256=aZdNyriRAzb6kYf_A3RSrU0AxYXwH5Zwsw54JxuU_2Q,364
|
|
17
|
+
alegra_e_provider-0.0.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
18
|
+
alegra_e_provider-0.0.1.dist-info/top_level.txt,sha256=NcYktmnWNtk9MEfrDm6zdm260Udm6SmW6gEczIgEefw,7
|
|
19
|
+
alegra_e_provider-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
alegra
|