constec 0.7.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.
- constec/db/__init__.py +11 -0
- constec/db/apps.py +8 -0
- constec/db/migrations/0001_initial.py +551 -0
- constec/db/migrations/0002_module_level.py +38 -0
- constec/db/migrations/0003_remove_module_level.py +15 -0
- constec/db/migrations/0004_rename_entities_company_cuit_idx_entities_company_e2c50f_idx_and_more.py +345 -0
- constec/db/migrations/0005_event.py +46 -0
- constec/db/migrations/0006_automation_trigger_action_executionlog_notificationtemplate.py +275 -0
- constec/db/migrations/0007_add_organization_to_automations.py +91 -0
- constec/db/migrations/0008_refactor_creator_fields.py +173 -0
- constec/db/migrations/0009_rename_user_to_companyuser.py +40 -0
- constec/db/migrations/__init__.py +0 -0
- constec/db/models/__init__.py +110 -0
- constec/db/models/automation.py +488 -0
- constec/db/models/base.py +23 -0
- constec/db/models/company.py +36 -0
- constec/db/models/contact.py +71 -0
- constec/db/models/erp.py +101 -0
- constec/db/models/erp_entity.py +122 -0
- constec/db/models/flow.py +138 -0
- constec/db/models/group.py +36 -0
- constec/db/models/module.py +67 -0
- constec/db/models/organization.py +62 -0
- constec/db/models/person.py +28 -0
- constec/db/models/session.py +89 -0
- constec/db/models/tag.py +70 -0
- constec/db/models/user.py +74 -0
- constec/py.typed +0 -0
- constec/services/__init__.py +14 -0
- constec/services/encryption.py +92 -0
- constec/shared/__init__.py +20 -0
- constec/shared/exceptions.py +48 -0
- constec/utils/__init__.py +20 -0
- constec/utils/cuit.py +107 -0
- constec/utils/password.py +62 -0
- constec-0.7.1.dist-info/METADATA +94 -0
- constec-0.7.1.dist-info/RECORD +40 -0
- constec-0.7.1.dist-info/WHEEL +5 -0
- constec-0.7.1.dist-info/licenses/LICENSE +21 -0
- constec-0.7.1.dist-info/top_level.txt +1 -0
constec/db/models/tag.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .person import Person
|
|
4
|
+
from .company import Company
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TagCategory(UUIDModel):
|
|
8
|
+
"""Tag categories per company."""
|
|
9
|
+
company = models.ForeignKey(
|
|
10
|
+
Company,
|
|
11
|
+
on_delete=models.CASCADE,
|
|
12
|
+
related_name="tag_categories",
|
|
13
|
+
)
|
|
14
|
+
name = models.CharField(max_length=50)
|
|
15
|
+
description = models.TextField(blank=True, null=True)
|
|
16
|
+
|
|
17
|
+
class Meta:
|
|
18
|
+
app_label = 'constec_db'
|
|
19
|
+
db_table = 'core"."tag_categories'
|
|
20
|
+
verbose_name = 'Tag category'
|
|
21
|
+
verbose_name_plural = 'Tag categories'
|
|
22
|
+
|
|
23
|
+
def __str__(self):
|
|
24
|
+
return f"{self.name}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PersonTag(UUIDModel):
|
|
28
|
+
"""Tags for persons."""
|
|
29
|
+
company = models.ForeignKey(
|
|
30
|
+
Company,
|
|
31
|
+
on_delete=models.CASCADE,
|
|
32
|
+
related_name="person_tags",
|
|
33
|
+
)
|
|
34
|
+
category = models.ForeignKey(
|
|
35
|
+
TagCategory,
|
|
36
|
+
on_delete=models.CASCADE,
|
|
37
|
+
related_name="person_tags",
|
|
38
|
+
)
|
|
39
|
+
name = models.CharField(max_length=50)
|
|
40
|
+
color = models.CharField(max_length=7, blank=True, null=True)
|
|
41
|
+
|
|
42
|
+
class Meta:
|
|
43
|
+
app_label = 'constec_db'
|
|
44
|
+
db_table = 'core"."person_tags'
|
|
45
|
+
unique_together = [['company', 'category', 'name']]
|
|
46
|
+
|
|
47
|
+
def __str__(self):
|
|
48
|
+
return f"{self.name}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PersonTagged(UUIDModel):
|
|
52
|
+
"""Person-tag relationship."""
|
|
53
|
+
person = models.ForeignKey(
|
|
54
|
+
Person,
|
|
55
|
+
on_delete=models.CASCADE,
|
|
56
|
+
related_name="tagged_items",
|
|
57
|
+
)
|
|
58
|
+
tag = models.ForeignKey(
|
|
59
|
+
PersonTag,
|
|
60
|
+
on_delete=models.CASCADE,
|
|
61
|
+
related_name="tagged_persons",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
class Meta:
|
|
65
|
+
app_label = 'constec_db'
|
|
66
|
+
db_table = 'core"."person_tagged'
|
|
67
|
+
unique_together = [['person', 'tag']]
|
|
68
|
+
|
|
69
|
+
def __str__(self):
|
|
70
|
+
return f"{self.person.full_name} - {self.tag.name}"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .company import Company
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CompanyUser(UUIDModel):
|
|
7
|
+
"""User belonging to a company."""
|
|
8
|
+
company = models.ForeignKey(
|
|
9
|
+
Company,
|
|
10
|
+
on_delete=models.CASCADE,
|
|
11
|
+
related_name="users"
|
|
12
|
+
)
|
|
13
|
+
name = models.CharField(max_length=255)
|
|
14
|
+
email = models.EmailField(unique=True)
|
|
15
|
+
password_hash = models.CharField(max_length=255)
|
|
16
|
+
|
|
17
|
+
class Meta:
|
|
18
|
+
app_label = 'constec_db'
|
|
19
|
+
db_table = 'core"."company_users'
|
|
20
|
+
|
|
21
|
+
def __str__(self):
|
|
22
|
+
return f"{self.name} ({self.email})"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CompanyUserRole(UUIDModel):
|
|
26
|
+
"""Role within a company."""
|
|
27
|
+
company = models.ForeignKey(
|
|
28
|
+
Company,
|
|
29
|
+
on_delete=models.CASCADE,
|
|
30
|
+
related_name="user_roles",
|
|
31
|
+
null=True,
|
|
32
|
+
blank=True
|
|
33
|
+
)
|
|
34
|
+
name = models.CharField(max_length=100)
|
|
35
|
+
description = models.TextField(blank=True, null=True)
|
|
36
|
+
permissions = models.JSONField(default=dict)
|
|
37
|
+
is_system_role = models.BooleanField(default=False)
|
|
38
|
+
|
|
39
|
+
class Meta:
|
|
40
|
+
app_label = 'constec_db'
|
|
41
|
+
db_table = 'core"."company_user_roles'
|
|
42
|
+
unique_together = [['company', 'name']]
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
return f"{self.name}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CompanyUserAccess(UUIDModel):
|
|
49
|
+
"""Cross-company access: one CompanyUser can access multiple Companies."""
|
|
50
|
+
user = models.ForeignKey(
|
|
51
|
+
CompanyUser,
|
|
52
|
+
on_delete=models.CASCADE,
|
|
53
|
+
related_name="company_accesses",
|
|
54
|
+
)
|
|
55
|
+
company = models.ForeignKey(
|
|
56
|
+
Company,
|
|
57
|
+
on_delete=models.CASCADE,
|
|
58
|
+
related_name="user_accesses",
|
|
59
|
+
)
|
|
60
|
+
role = models.ForeignKey(
|
|
61
|
+
CompanyUserRole,
|
|
62
|
+
on_delete=models.PROTECT,
|
|
63
|
+
related_name="user_accesses",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
class Meta:
|
|
67
|
+
app_label = 'constec_db'
|
|
68
|
+
db_table = 'core"."company_user_access'
|
|
69
|
+
verbose_name = 'Company user access'
|
|
70
|
+
verbose_name_plural = 'Company user access'
|
|
71
|
+
unique_together = [['user', 'company']]
|
|
72
|
+
|
|
73
|
+
def __str__(self):
|
|
74
|
+
return f"{self.user.email} @ {self.company.name} ({self.role.name})"
|
constec/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
constec.services - Shared services for the Constec platform.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from constec.services import encrypt_password, decrypt_password
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .encryption import encrypt_password, decrypt_password, EncryptionService
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'encrypt_password',
|
|
12
|
+
'decrypt_password',
|
|
13
|
+
'EncryptionService',
|
|
14
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fernet encryption for sensitive data like database passwords.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from constec.services import encrypt_password, decrypt_password
|
|
6
|
+
|
|
7
|
+
# Encrypt
|
|
8
|
+
encrypted = encrypt_password("my_password", fernet_key)
|
|
9
|
+
|
|
10
|
+
# Decrypt
|
|
11
|
+
password = decrypt_password(encrypted, fernet_key)
|
|
12
|
+
|
|
13
|
+
The FERNET_KEY should be stored in environment variables and never committed.
|
|
14
|
+
Generate a new key with:
|
|
15
|
+
from cryptography.fernet import Fernet
|
|
16
|
+
print(Fernet.generate_key().decode())
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EncryptionService:
|
|
23
|
+
"""Service for encrypting and decrypting sensitive data."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, key: str):
|
|
26
|
+
"""
|
|
27
|
+
Initialize with Fernet key.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
key: Fernet key as string (will be encoded to bytes)
|
|
31
|
+
"""
|
|
32
|
+
self._fernet = Fernet(key.encode() if isinstance(key, str) else key)
|
|
33
|
+
|
|
34
|
+
def encrypt(self, plaintext: str) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Encrypt a string.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
plaintext: String to encrypt
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Encrypted string (base64 encoded)
|
|
43
|
+
"""
|
|
44
|
+
return self._fernet.encrypt(plaintext.encode()).decode()
|
|
45
|
+
|
|
46
|
+
def decrypt(self, ciphertext: str) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Decrypt a string.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
ciphertext: Encrypted string (base64 encoded)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Decrypted string
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
InvalidToken: If decryption fails (wrong key or corrupted data)
|
|
58
|
+
"""
|
|
59
|
+
return self._fernet.decrypt(ciphertext.encode()).decode()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def encrypt_password(password: str, fernet_key: str) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Encrypt a password using Fernet symmetric encryption.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
password: Plain text password
|
|
68
|
+
fernet_key: Fernet encryption key
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Encrypted password (base64 encoded string)
|
|
72
|
+
"""
|
|
73
|
+
service = EncryptionService(fernet_key)
|
|
74
|
+
return service.encrypt(password)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def decrypt_password(encrypted_password: str, fernet_key: str) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Decrypt a password using Fernet symmetric encryption.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
encrypted_password: Encrypted password (base64 encoded string)
|
|
83
|
+
fernet_key: Fernet encryption key
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Decrypted password
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
InvalidToken: If decryption fails
|
|
90
|
+
"""
|
|
91
|
+
service = EncryptionService(fernet_key)
|
|
92
|
+
return service.decrypt(encrypted_password)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utilities for the Constec library ecosystem.
|
|
3
|
+
"""
|
|
4
|
+
from constec.shared.exceptions import (
|
|
5
|
+
ConstecError,
|
|
6
|
+
ConstecAPIError,
|
|
7
|
+
ConstecConnectionError,
|
|
8
|
+
ConstecValidationError,
|
|
9
|
+
ConstecAuthenticationError,
|
|
10
|
+
ConstecNotFoundError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ConstecError",
|
|
15
|
+
"ConstecAPIError",
|
|
16
|
+
"ConstecConnectionError",
|
|
17
|
+
"ConstecValidationError",
|
|
18
|
+
"ConstecAuthenticationError",
|
|
19
|
+
"ConstecNotFoundError",
|
|
20
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared exception classes for the Constec library ecosystem.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConstecError(Exception):
|
|
8
|
+
"""Base exception for all Constec-related errors."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str, details: Optional[dict] = None):
|
|
11
|
+
self.message = message
|
|
12
|
+
self.details = details or {}
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
|
|
15
|
+
def __str__(self):
|
|
16
|
+
if self.details:
|
|
17
|
+
return f"{self.message} (Details: {self.details})"
|
|
18
|
+
return self.message
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConstecAPIError(ConstecError):
|
|
22
|
+
"""Raised when API requests fail."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, message: str, status_code: Optional[int] = None, response_data: Optional[dict] = None):
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
self.response_data = response_data or {}
|
|
27
|
+
details = {"status_code": status_code, "response": response_data}
|
|
28
|
+
super().__init__(message, details)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConstecConnectionError(ConstecError):
|
|
32
|
+
"""Raised when connection to a service fails."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConstecValidationError(ConstecError):
|
|
37
|
+
"""Raised when data validation fails."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConstecAuthenticationError(ConstecAPIError):
|
|
42
|
+
"""Raised when authentication fails."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ConstecNotFoundError(ConstecAPIError):
|
|
47
|
+
"""Raised when a requested resource is not found."""
|
|
48
|
+
pass
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
constec.utils - Shared utilities for the Constec platform.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from constec.utils import normalize_cuit, validate_cuit
|
|
6
|
+
from constec.utils import hash_password, verify_password
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .cuit import normalize_cuit, validate_cuit, format_cuit
|
|
10
|
+
from .password import hash_password, verify_password
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
# CUIT
|
|
14
|
+
'normalize_cuit',
|
|
15
|
+
'validate_cuit',
|
|
16
|
+
'format_cuit',
|
|
17
|
+
# Password
|
|
18
|
+
'hash_password',
|
|
19
|
+
'verify_password',
|
|
20
|
+
]
|
constec/utils/cuit.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CUIT (Clave Única de Identificación Tributaria) utilities.
|
|
3
|
+
|
|
4
|
+
Argentine tax identification number format: XX-XXXXXXXX-X
|
|
5
|
+
- First 2 digits: Type (20=person, 23/24=company, 27=foreign, 30/33/34=company)
|
|
6
|
+
- Middle 8 digits: ID number
|
|
7
|
+
- Last digit: Verification digit
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from constec.utils import normalize_cuit, validate_cuit, format_cuit
|
|
11
|
+
|
|
12
|
+
# Normalize (remove dashes)
|
|
13
|
+
normalized = normalize_cuit("20-12345678-9") # "20123456789"
|
|
14
|
+
|
|
15
|
+
# Validate
|
|
16
|
+
is_valid = validate_cuit("20-12345678-9") # True/False
|
|
17
|
+
|
|
18
|
+
# Format (add dashes)
|
|
19
|
+
formatted = format_cuit("20123456789") # "20-12345678-9"
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_cuit(cuit: str) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Normalize CUIT by removing dashes and spaces.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
cuit: CUIT string (with or without dashes)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
CUIT without dashes (11 digits)
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
>>> normalize_cuit("20-12345678-9")
|
|
37
|
+
'20123456789'
|
|
38
|
+
>>> normalize_cuit("20 12345678 9")
|
|
39
|
+
'20123456789'
|
|
40
|
+
"""
|
|
41
|
+
return re.sub(r'[\s\-]', '', cuit)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def format_cuit(cuit: str) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Format CUIT with dashes (XX-XXXXXXXX-X).
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
cuit: CUIT string (11 digits, with or without dashes)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Formatted CUIT with dashes
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
>>> format_cuit("20123456789")
|
|
56
|
+
'20-12345678-9'
|
|
57
|
+
"""
|
|
58
|
+
normalized = normalize_cuit(cuit)
|
|
59
|
+
if len(normalized) != 11:
|
|
60
|
+
return cuit # Return as-is if invalid length
|
|
61
|
+
return f"{normalized[:2]}-{normalized[2:10]}-{normalized[10]}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _calculate_verification_digit(cuit_base: str) -> int:
|
|
65
|
+
"""Calculate the CUIT verification digit using mod 11 algorithm."""
|
|
66
|
+
weights = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2]
|
|
67
|
+
total = sum(int(d) * w for d, w in zip(cuit_base, weights))
|
|
68
|
+
remainder = total % 11
|
|
69
|
+
|
|
70
|
+
if remainder == 0:
|
|
71
|
+
return 0
|
|
72
|
+
elif remainder == 1:
|
|
73
|
+
return 9 # Special case for type 23 (women)
|
|
74
|
+
else:
|
|
75
|
+
return 11 - remainder
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def validate_cuit(cuit: str) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Validate a CUIT using the verification digit algorithm.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
cuit: CUIT string (with or without dashes)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if valid, False otherwise
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
>>> validate_cuit("20-12345678-9")
|
|
90
|
+
True # (if verification digit is correct)
|
|
91
|
+
"""
|
|
92
|
+
normalized = normalize_cuit(cuit)
|
|
93
|
+
|
|
94
|
+
# Must be exactly 11 digits
|
|
95
|
+
if not re.match(r'^\d{11}$', normalized):
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
# Valid type prefixes
|
|
99
|
+
valid_types = ['20', '23', '24', '27', '30', '33', '34']
|
|
100
|
+
if normalized[:2] not in valid_types:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
# Verify check digit
|
|
104
|
+
expected_digit = _calculate_verification_digit(normalized[:10])
|
|
105
|
+
actual_digit = int(normalized[10])
|
|
106
|
+
|
|
107
|
+
return expected_digit == actual_digit
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Password hashing utilities using bcrypt.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from constec.utils import hash_password, verify_password
|
|
6
|
+
|
|
7
|
+
# Hash a password
|
|
8
|
+
hashed = hash_password("my_password")
|
|
9
|
+
|
|
10
|
+
# Verify a password
|
|
11
|
+
is_valid = verify_password("my_password", hashed) # True
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import bcrypt
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def hash_password(password: str, rounds: int = 12) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Hash a password using bcrypt.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
password: Plain text password
|
|
23
|
+
rounds: Cost factor (default 12, higher = slower but more secure)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Bcrypt hash as string
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> hashed = hash_password("secret123")
|
|
30
|
+
>>> hashed.startswith("$2b$")
|
|
31
|
+
True
|
|
32
|
+
"""
|
|
33
|
+
salt = bcrypt.gensalt(rounds=rounds)
|
|
34
|
+
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
|
35
|
+
return hashed.decode('utf-8')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def verify_password(password: str, hashed: str) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Verify a password against a bcrypt hash.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
password: Plain text password to verify
|
|
44
|
+
hashed: Bcrypt hash to verify against
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if password matches, False otherwise
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> hashed = hash_password("secret123")
|
|
51
|
+
>>> verify_password("secret123", hashed)
|
|
52
|
+
True
|
|
53
|
+
>>> verify_password("wrong", hashed)
|
|
54
|
+
False
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
return bcrypt.checkpw(
|
|
58
|
+
password.encode('utf-8'),
|
|
59
|
+
hashed.encode('utf-8')
|
|
60
|
+
)
|
|
61
|
+
except (ValueError, TypeError):
|
|
62
|
+
return False
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: constec
|
|
3
|
+
Version: 0.7.1
|
|
4
|
+
Summary: Base library for the Constec ecosystem - shared utilities, models, and namespace foundation
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/TpmyCT/constec-python
|
|
7
|
+
Project-URL: Repository, https://github.com/TpmyCT/constec-python
|
|
8
|
+
Project-URL: Issues, https://github.com/TpmyCT/constec-python/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Classifier: Framework :: Django :: 4.2
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: db
|
|
23
|
+
Requires-Dist: Django>=4.2; extra == "db"
|
|
24
|
+
Provides-Extra: services
|
|
25
|
+
Requires-Dist: cryptography>=41.0; extra == "services"
|
|
26
|
+
Requires-Dist: bcrypt>=4.0; extra == "services"
|
|
27
|
+
Provides-Extra: utils
|
|
28
|
+
Requires-Dist: bcrypt>=4.0; extra == "utils"
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: constec[db,services,utils]; extra == "all"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# Constec
|
|
34
|
+
|
|
35
|
+
Base library for the Constec ecosystem - provides shared utilities for working with Constec services.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install constec
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What is this?
|
|
44
|
+
|
|
45
|
+
This is a foundational library that provides common utilities used across Constec client libraries. Most users will install this automatically as a dependency when installing specific Constec client packages like:
|
|
46
|
+
|
|
47
|
+
- **constec-erp** - Client for the Constec ERP API
|
|
48
|
+
|
|
49
|
+
If you're looking to interact with Constec services, install the specific client library you need instead of this base package.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
This library provides exception classes for error handling when working with Constec APIs:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from constec.shared import (
|
|
57
|
+
ConstecError,
|
|
58
|
+
ConstecAPIError,
|
|
59
|
+
ConstecConnectionError,
|
|
60
|
+
ConstecValidationError,
|
|
61
|
+
ConstecAuthenticationError,
|
|
62
|
+
ConstecNotFoundError,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Handle errors from Constec services
|
|
66
|
+
try:
|
|
67
|
+
# Your Constec API calls here
|
|
68
|
+
pass
|
|
69
|
+
except ConstecAuthenticationError:
|
|
70
|
+
print("Authentication failed - check your credentials")
|
|
71
|
+
except ConstecNotFoundError:
|
|
72
|
+
print("Resource not found")
|
|
73
|
+
except ConstecAPIError as e:
|
|
74
|
+
print(f"API error: {e.message}")
|
|
75
|
+
if e.status_code:
|
|
76
|
+
print(f"Status code: {e.status_code}")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Available Exceptions
|
|
80
|
+
|
|
81
|
+
- `ConstecError` - Base exception for all Constec errors
|
|
82
|
+
- `ConstecAPIError` - API request failures (includes status code and response data)
|
|
83
|
+
- `ConstecAuthenticationError` - Authentication failures
|
|
84
|
+
- `ConstecNotFoundError` - Resource not found (404)
|
|
85
|
+
- `ConstecConnectionError` - Connection failures
|
|
86
|
+
- `ConstecValidationError` - Data validation errors
|
|
87
|
+
|
|
88
|
+
## Requirements
|
|
89
|
+
|
|
90
|
+
- Python 3.9 or higher
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
constec/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
constec/db/__init__.py,sha256=wou_y3aqCg1xP8Gr13FKRy0kEI2avV-19G3P2g3bzjs,287
|
|
3
|
+
constec/db/apps.py,sha256=zc-9lGNa049q9bvxV7957EwnZjuBozPLZq594cKAU24,221
|
|
4
|
+
constec/db/migrations/0001_initial.py,sha256=ru0auCkdz83Qu3e7hGX24WjZXAlM6fE7RZtgAnzANzQ,28979
|
|
5
|
+
constec/db/migrations/0002_module_level.py,sha256=Es27KUOuI_qrlyL8xBTjwsJJHVduAErNxn4Sdi0a-0w,1526
|
|
6
|
+
constec/db/migrations/0003_remove_module_level.py,sha256=3GX-VDLSZqqxaZf6KFtuc7U9nCkQ9EF8pSDxOnCz63g,275
|
|
7
|
+
constec/db/migrations/0004_rename_entities_company_cuit_idx_entities_company_e2c50f_idx_and_more.py,sha256=TCkutatVjJhVKxiQwp-RLhBlbOo5rtwSV1qQqao1qCI,11874
|
|
8
|
+
constec/db/migrations/0005_event.py,sha256=kyvEFOAbOJj3daDzOWcIhfXD8gl5EpIHYvRy3880cZc,2892
|
|
9
|
+
constec/db/migrations/0006_automation_trigger_action_executionlog_notificationtemplate.py,sha256=sfh1YTRPqUiBb5FIxRNJm_BnlYkDonwm7FEEihgtSe0,13628
|
|
10
|
+
constec/db/migrations/0007_add_organization_to_automations.py,sha256=08yDuetrA4WWwqNriyewr0qCpesLo_9x5YD36CDScj4,3467
|
|
11
|
+
constec/db/migrations/0008_refactor_creator_fields.py,sha256=O_bcChxFR_IPw1BV_1NDbsIdrzhlj4MfpdhPXnhdau4,6381
|
|
12
|
+
constec/db/migrations/0009_rename_user_to_companyuser.py,sha256=2UvFpelG88s1NRKQ-Q3-Uz9Cmp5D_HdLHrLGElwq5_c,1052
|
|
13
|
+
constec/db/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
constec/db/models/__init__.py,sha256=zEUX56xTEGi09ZVcRwDzSnPeKPv1Dl7pIZdAzlLhD-U,2686
|
|
15
|
+
constec/db/models/automation.py,sha256=abTxEg2Ko5H3rybFvGUWeoy0hcEsLm5t8khpUFqLMQM,15027
|
|
16
|
+
constec/db/models/base.py,sha256=Urbz9iyOtSxdTfgtEYbtBLmTiea2QAcLq0L40tAzEq8,736
|
|
17
|
+
constec/db/models/company.py,sha256=APYsbiNoqQ-73wxFP0jq3AByHNHn3Jjbe_J_iwpD9gM,1047
|
|
18
|
+
constec/db/models/contact.py,sha256=VJ66YgzCqp09dzYM5IK9rZZV1jqBepJQO8YK28gA98k,2106
|
|
19
|
+
constec/db/models/erp.py,sha256=kTh_MYVwuxS32li2YTCTOgygEVc0UE9CTqnKbfkSkvQ,3060
|
|
20
|
+
constec/db/models/erp_entity.py,sha256=1bV5vhPz89aThKf_YP3YW-baQhSsER04byXnZkwMJd0,3715
|
|
21
|
+
constec/db/models/flow.py,sha256=AdWRCvDo2ZRK_uRbSf-VLZQZ1aVJ-u8X1Ohe9cg3S2M,4093
|
|
22
|
+
constec/db/models/group.py,sha256=RllL0pV5jHreeZF17ycFUJdXBxpQG_n_VFCBsQglcz8,911
|
|
23
|
+
constec/db/models/module.py,sha256=z3TYqk0-9VhCYB9eYz-AH1FRSyKnv6aUbnCM6pjGrdA,1919
|
|
24
|
+
constec/db/models/organization.py,sha256=9dbsh5UBShtDRAeyMqqQEPEaflhzS3aiZpRWUiEluBU,1731
|
|
25
|
+
constec/db/models/person.py,sha256=B4BNdy2AgqmxrZrkWWvqnFxFHs4uO1n61Y_5sP6MsUo,848
|
|
26
|
+
constec/db/models/session.py,sha256=CSWiC2Jo2SZnXocy4fQci5qcfLTyaLM2H381iOr6yzg,2477
|
|
27
|
+
constec/db/models/tag.py,sha256=VlddQW8MLDtYmtt6wp7kC6MsT6SQ6odiJX8mUK8FP3o,1815
|
|
28
|
+
constec/db/models/user.py,sha256=UvQ6iLCp2ZBuYddV9YFuJ-v8GTmMh84wO7zN3SSadSA,2069
|
|
29
|
+
constec/services/__init__.py,sha256=LXGKIzaRf1gVG2y7oUc18EulUsJXHVs8-oz0nY2h7WA,312
|
|
30
|
+
constec/services/encryption.py,sha256=rwfAGUC7kDy1bhezHPoZG1hTjajpEB6l3AgcfGtmdvg,2368
|
|
31
|
+
constec/shared/__init__.py,sha256=Qe99OxBfwg3_4i4zyCQR20vCcyyaIULpLzUa0XfwBPg,435
|
|
32
|
+
constec/shared/exceptions.py,sha256=8Bih40RWoH0gVhto09mH2ppSQV_drHPnGWITcoD-0J0,1335
|
|
33
|
+
constec/utils/__init__.py,sha256=brf-G4UvU-3CK_rKNPTaHwsVsxnoJSbml_QTZJSM7d0,458
|
|
34
|
+
constec/utils/cuit.py,sha256=dQKGlA4pRQ5DyR-N4BiV8ZsvAle2Vgjif7PU72zHx_A,2753
|
|
35
|
+
constec/utils/password.py,sha256=XNpTJ9xZQSoZNjXEAnexAEZuYkwW1dFgX4AY-B5Q0gA,1462
|
|
36
|
+
constec-0.7.1.dist-info/licenses/LICENSE,sha256=a1R51ONDGq0UQfV-n3ybsNL7EGhcC2sQ1sXvRANaFVI,1064
|
|
37
|
+
constec-0.7.1.dist-info/METADATA,sha256=hrtu4CNyCT4V4ETsnv7_y19vX1G6UmSMFvDfMZMX35E,2954
|
|
38
|
+
constec-0.7.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
39
|
+
constec-0.7.1.dist-info/top_level.txt,sha256=bQ9AydOLlthShsr7tA7t7ivbLvlLPdhHOo0BdWgnh_Y,8
|
|
40
|
+
constec-0.7.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Constec
|
|
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 @@
|
|
|
1
|
+
constec
|