constec 0.1.0__py3-none-any.whl → 0.2.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.
- constec/db/__init__.py +11 -0
- constec/db/apps.py +8 -0
- constec/db/migrations/0001_initial.py +538 -0
- constec/db/migrations/0002_alter_companysystem_table_alter_connection_table_and_more.py +37 -0
- constec/db/migrations/0003_add_is_active_to_company.py +18 -0
- constec/db/migrations/__init__.py +0 -0
- constec/db/models/__init__.py +91 -0
- constec/db/models/base.py +14 -0
- constec/db/models/company.py +36 -0
- constec/db/models/contact.py +68 -0
- constec/db/models/erp.py +109 -0
- constec/db/models/erp_entity.py +113 -0
- constec/db/models/flow.py +145 -0
- constec/db/models/group.py +36 -0
- constec/db/models/module.py +43 -0
- constec/db/models/organization.py +62 -0
- constec/db/models/person.py +28 -0
- constec/db/models/session.py +94 -0
- constec/db/models/tag.py +68 -0
- constec/db/models/user.py +72 -0
- constec/py.typed +0 -0
- constec/services/__init__.py +14 -0
- constec/services/encryption.py +92 -0
- constec/utils/__init__.py +20 -0
- constec/utils/cuit.py +107 -0
- constec/utils/password.py +62 -0
- {constec-0.1.0.dist-info → constec-0.2.0.dist-info}/METADATA +12 -2
- constec-0.2.0.dist-info/RECORD +33 -0
- {constec-0.1.0.dist-info → constec-0.2.0.dist-info}/WHEEL +1 -1
- constec-0.1.0.dist-info/RECORD +0 -7
- {constec-0.1.0.dist-info → constec-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {constec-0.1.0.dist-info → constec-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Organization(UUIDModel):
|
|
6
|
+
"""Platform-level tenant (e.g., Constec itself)."""
|
|
7
|
+
name = models.CharField(max_length=255)
|
|
8
|
+
slug = models.SlugField(max_length=100, unique=True)
|
|
9
|
+
description = models.TextField(blank=True, null=True)
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
app_label = 'constec_db'
|
|
13
|
+
db_table = 'core"."organizations'
|
|
14
|
+
|
|
15
|
+
def __str__(self):
|
|
16
|
+
return self.name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OrganizationRole(UUIDModel):
|
|
20
|
+
"""Roles within an organization."""
|
|
21
|
+
organization = models.ForeignKey(
|
|
22
|
+
Organization,
|
|
23
|
+
on_delete=models.CASCADE,
|
|
24
|
+
related_name="organization_roles",
|
|
25
|
+
null=True,
|
|
26
|
+
blank=True
|
|
27
|
+
)
|
|
28
|
+
name = models.CharField(max_length=100)
|
|
29
|
+
description = models.TextField(blank=True, null=True)
|
|
30
|
+
permissions = models.JSONField(default=dict)
|
|
31
|
+
is_system_role = models.BooleanField(default=False)
|
|
32
|
+
|
|
33
|
+
class Meta:
|
|
34
|
+
app_label = 'constec_db'
|
|
35
|
+
db_table = 'core"."organization_roles'
|
|
36
|
+
unique_together = [['organization', 'name']]
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return self.name
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OrganizationUser(UUIDModel):
|
|
43
|
+
"""Administrator users at organization level."""
|
|
44
|
+
organization = models.ForeignKey(
|
|
45
|
+
Organization,
|
|
46
|
+
on_delete=models.CASCADE,
|
|
47
|
+
related_name="users",
|
|
48
|
+
)
|
|
49
|
+
name = models.CharField(max_length=255)
|
|
50
|
+
email = models.EmailField(unique=True)
|
|
51
|
+
password_hash = models.CharField(max_length=255)
|
|
52
|
+
role = models.ForeignKey(
|
|
53
|
+
OrganizationRole,
|
|
54
|
+
on_delete=models.PROTECT
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
class Meta:
|
|
58
|
+
app_label = 'constec_db'
|
|
59
|
+
db_table = 'core"."organization_users'
|
|
60
|
+
|
|
61
|
+
def __str__(self):
|
|
62
|
+
return f"{self.name} ({self.email})"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .company import Company
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Person(UUIDModel):
|
|
7
|
+
"""Real person (customer, supplier, employee)."""
|
|
8
|
+
company = models.ForeignKey(
|
|
9
|
+
Company,
|
|
10
|
+
on_delete=models.CASCADE,
|
|
11
|
+
related_name="persons",
|
|
12
|
+
)
|
|
13
|
+
first_name = models.CharField(max_length=100)
|
|
14
|
+
last_name = models.CharField(max_length=100)
|
|
15
|
+
full_name = models.CharField(max_length=255)
|
|
16
|
+
metadata = models.JSONField(default=dict, blank=True)
|
|
17
|
+
|
|
18
|
+
class Meta:
|
|
19
|
+
app_label = 'constec_db'
|
|
20
|
+
db_table = 'core"."persons'
|
|
21
|
+
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return f"{self.full_name}"
|
|
24
|
+
|
|
25
|
+
def save(self, *args, **kwargs):
|
|
26
|
+
"""Auto-generate full_name from first_name and last_name."""
|
|
27
|
+
self.full_name = f"{self.first_name} {self.last_name}"
|
|
28
|
+
super().save(*args, **kwargs)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .company import Company
|
|
4
|
+
from .person import Person
|
|
5
|
+
from .flow import Flow
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Session(UUIDModel):
|
|
9
|
+
"""Conversation session between a user and the AI agent.
|
|
10
|
+
|
|
11
|
+
Tracks the conversation context including company, user, and permissions.
|
|
12
|
+
All MCP tools receive the session_id to know who is making the request.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
company = models.ForeignKey(
|
|
16
|
+
Company,
|
|
17
|
+
on_delete=models.CASCADE,
|
|
18
|
+
related_name="sessions",
|
|
19
|
+
help_text="The company this session belongs to"
|
|
20
|
+
)
|
|
21
|
+
user = models.ForeignKey(
|
|
22
|
+
Person,
|
|
23
|
+
on_delete=models.CASCADE,
|
|
24
|
+
related_name="sessions",
|
|
25
|
+
help_text="The person chatting (can be employee or client)"
|
|
26
|
+
)
|
|
27
|
+
flow = models.ForeignKey(
|
|
28
|
+
Flow,
|
|
29
|
+
on_delete=models.PROTECT,
|
|
30
|
+
related_name="sessions",
|
|
31
|
+
help_text="AI flow configuration to use"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
metadata = models.JSONField(
|
|
35
|
+
default=dict,
|
|
36
|
+
help_text="Session context (connection, role, etc.)"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
is_active = models.BooleanField(default=True)
|
|
40
|
+
ended_at = models.DateTimeField(null=True, blank=True)
|
|
41
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
42
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
43
|
+
|
|
44
|
+
class Meta:
|
|
45
|
+
app_label = 'constec_db'
|
|
46
|
+
db_table = 'constancia"."sessions'
|
|
47
|
+
indexes = [
|
|
48
|
+
models.Index(fields=['company', 'is_active']),
|
|
49
|
+
models.Index(fields=['user', 'is_active']),
|
|
50
|
+
models.Index(fields=['-created_at']),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
def __str__(self):
|
|
54
|
+
return f"Session {self.id} - {self.user} @ {self.company.name}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Message(UUIDModel):
|
|
58
|
+
"""Message in a conversation session.
|
|
59
|
+
|
|
60
|
+
Stores the conversation history between user and AI assistant.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
ROLE_CHOICES = [
|
|
64
|
+
('user', 'User'),
|
|
65
|
+
('assistant', 'Assistant'),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
session = models.ForeignKey(
|
|
69
|
+
Session,
|
|
70
|
+
on_delete=models.CASCADE,
|
|
71
|
+
related_name="messages",
|
|
72
|
+
)
|
|
73
|
+
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
|
|
74
|
+
content = models.TextField()
|
|
75
|
+
|
|
76
|
+
metadata = models.JSONField(
|
|
77
|
+
default=dict,
|
|
78
|
+
blank=True,
|
|
79
|
+
help_text="Additional message data: tool calls, errors, context, etc."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
83
|
+
|
|
84
|
+
class Meta:
|
|
85
|
+
app_label = 'constec_db'
|
|
86
|
+
db_table = 'constancia"."messages'
|
|
87
|
+
ordering = ['created_at']
|
|
88
|
+
indexes = [
|
|
89
|
+
models.Index(fields=['session', 'created_at']),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
def __str__(self):
|
|
93
|
+
preview = self.content[:50] + "..." if len(self.content) > 50 else self.content
|
|
94
|
+
return f"{self.role}: {preview}"
|
constec/db/models/tag.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
|
|
21
|
+
def __str__(self):
|
|
22
|
+
return f"{self.name}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PersonTag(UUIDModel):
|
|
26
|
+
"""Tags for persons."""
|
|
27
|
+
company = models.ForeignKey(
|
|
28
|
+
Company,
|
|
29
|
+
on_delete=models.CASCADE,
|
|
30
|
+
related_name="person_tags",
|
|
31
|
+
)
|
|
32
|
+
category = models.ForeignKey(
|
|
33
|
+
TagCategory,
|
|
34
|
+
on_delete=models.CASCADE,
|
|
35
|
+
related_name="person_tags",
|
|
36
|
+
)
|
|
37
|
+
name = models.CharField(max_length=50)
|
|
38
|
+
color = models.CharField(max_length=7, blank=True, null=True)
|
|
39
|
+
|
|
40
|
+
class Meta:
|
|
41
|
+
app_label = 'constec_db'
|
|
42
|
+
db_table = 'core"."person_tags'
|
|
43
|
+
unique_together = [['company', 'category', 'name']]
|
|
44
|
+
|
|
45
|
+
def __str__(self):
|
|
46
|
+
return f"{self.name}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PersonTagged(UUIDModel):
|
|
50
|
+
"""Person-tag relationship."""
|
|
51
|
+
person = models.ForeignKey(
|
|
52
|
+
Person,
|
|
53
|
+
on_delete=models.CASCADE,
|
|
54
|
+
related_name="tagged_items",
|
|
55
|
+
)
|
|
56
|
+
tag = models.ForeignKey(
|
|
57
|
+
PersonTag,
|
|
58
|
+
on_delete=models.CASCADE,
|
|
59
|
+
related_name="tagged_persons",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
class Meta:
|
|
63
|
+
app_label = 'constec_db'
|
|
64
|
+
db_table = 'core"."person_tagged'
|
|
65
|
+
unique_together = [['person', 'tag']]
|
|
66
|
+
|
|
67
|
+
def __str__(self):
|
|
68
|
+
return f"{self.person.full_name} - {self.tag.name}"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .company import Company
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class User(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"."users'
|
|
20
|
+
|
|
21
|
+
def __str__(self):
|
|
22
|
+
return f"{self.name} ({self.email})"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UserRole(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"."user_roles'
|
|
42
|
+
unique_together = [['company', 'name']]
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
return f"{self.name}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UserCompanyAccess(UUIDModel):
|
|
49
|
+
"""Cross-company access: one User can access multiple Companies."""
|
|
50
|
+
user = models.ForeignKey(
|
|
51
|
+
User,
|
|
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
|
+
UserRole,
|
|
62
|
+
on_delete=models.PROTECT,
|
|
63
|
+
related_name="user_accesses",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
class Meta:
|
|
67
|
+
app_label = 'constec_db'
|
|
68
|
+
db_table = 'core"."user_company_access'
|
|
69
|
+
unique_together = [['user', 'company']]
|
|
70
|
+
|
|
71
|
+
def __str__(self):
|
|
72
|
+
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
|
+
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: constec
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Base library for the Constec ecosystem - shared utilities and namespace foundation
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Base library for the Constec ecosystem - shared utilities, models, and namespace foundation
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/TpmyCT/constec-python
|
|
7
7
|
Project-URL: Repository, https://github.com/TpmyCT/constec-python
|
|
@@ -15,9 +15,19 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Typing :: Typed
|
|
18
|
+
Classifier: Framework :: Django :: 4.2
|
|
18
19
|
Requires-Python: >=3.9
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
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"
|
|
21
31
|
Dynamic: license-file
|
|
22
32
|
|
|
23
33
|
# Constec
|
|
@@ -0,0 +1,33 @@
|
|
|
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=OdNHJg1IgsjcpaFgolFG9e5HFEQZkuoFtJ81pi1UZ5U,28224
|
|
5
|
+
constec/db/migrations/0002_alter_companysystem_table_alter_connection_table_and_more.py,sha256=e6ygltUq_ObFXykDn9I-aypqxtBvMQg-0NcuSMoqaOc,911
|
|
6
|
+
constec/db/migrations/0003_add_is_active_to_company.py,sha256=kfMpnxkkOuJH2W81GYoKmT3qHvooyi22BGuFOSVqbJY,433
|
|
7
|
+
constec/db/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
constec/db/models/__init__.py,sha256=xsER3dtbsj14mSb52v1Mf1bE0RzKbvrvwD6xti2RN5s,2132
|
|
9
|
+
constec/db/models/base.py,sha256=1t_eYXYyggoL5fKLcitRPKr1qUE1Ql3_sOmSHQMD3QE,359
|
|
10
|
+
constec/db/models/company.py,sha256=VZMt6gmsEWBL6VPUYzTPYOngBqL3mPYmZ6ZuCiz5Jeo,1055
|
|
11
|
+
constec/db/models/contact.py,sha256=qzkekZBNBtusFMpZRANxwKjtDN9qCxc4BMz-H7RyWM0,1904
|
|
12
|
+
constec/db/models/erp.py,sha256=sN0U0XCMNW-pc9CNLZG_kPZeWXznq_XU3ML2zJ3arjA,3518
|
|
13
|
+
constec/db/models/erp_entity.py,sha256=lJG0sU7YfehuxoqrxmpvuCSG0_GsardU5DvwBgMKsW8,3412
|
|
14
|
+
constec/db/models/flow.py,sha256=K_UA7E4zJ2-UPNp5H9e-lfPeGn0wDEiOl0cRal4Dmb8,4414
|
|
15
|
+
constec/db/models/group.py,sha256=ueCOIfpmzR683ojWf5vLb6IG_jawfVRM7IJcGK05I3Y,897
|
|
16
|
+
constec/db/models/module.py,sha256=na_7pJw8Q2D2UkF86ZwK5VdJnV87yqUOMBvaBLw4KgI,1218
|
|
17
|
+
constec/db/models/organization.py,sha256=9dbsh5UBShtDRAeyMqqQEPEaflhzS3aiZpRWUiEluBU,1731
|
|
18
|
+
constec/db/models/person.py,sha256=B4BNdy2AgqmxrZrkWWvqnFxFHs4uO1n61Y_5sP6MsUo,848
|
|
19
|
+
constec/db/models/session.py,sha256=HeIk5rK25IaxKBRX-XNuJ75ndjJZ08rAdSGTLvTwlLE,2695
|
|
20
|
+
constec/db/models/tag.py,sha256=wgJYoffXCO_-G7zhS1FLiQHFtJVbJKPV-wyRG3mwb74,1730
|
|
21
|
+
constec/db/models/user.py,sha256=UzCgb47ZVg1cBN_Y5CIpZJeHSC8JOAYT_0xZE3d294Y,1921
|
|
22
|
+
constec/services/__init__.py,sha256=LXGKIzaRf1gVG2y7oUc18EulUsJXHVs8-oz0nY2h7WA,312
|
|
23
|
+
constec/services/encryption.py,sha256=rwfAGUC7kDy1bhezHPoZG1hTjajpEB6l3AgcfGtmdvg,2368
|
|
24
|
+
constec/shared/__init__.py,sha256=Qe99OxBfwg3_4i4zyCQR20vCcyyaIULpLzUa0XfwBPg,435
|
|
25
|
+
constec/shared/exceptions.py,sha256=8Bih40RWoH0gVhto09mH2ppSQV_drHPnGWITcoD-0J0,1335
|
|
26
|
+
constec/utils/__init__.py,sha256=brf-G4UvU-3CK_rKNPTaHwsVsxnoJSbml_QTZJSM7d0,458
|
|
27
|
+
constec/utils/cuit.py,sha256=dQKGlA4pRQ5DyR-N4BiV8ZsvAle2Vgjif7PU72zHx_A,2753
|
|
28
|
+
constec/utils/password.py,sha256=XNpTJ9xZQSoZNjXEAnexAEZuYkwW1dFgX4AY-B5Q0gA,1462
|
|
29
|
+
constec-0.2.0.dist-info/licenses/LICENSE,sha256=a1R51ONDGq0UQfV-n3ybsNL7EGhcC2sQ1sXvRANaFVI,1064
|
|
30
|
+
constec-0.2.0.dist-info/METADATA,sha256=Qa43yajTCNkSvEodrX0OmBs6iy27qVrvR_LxE-e7CVU,2954
|
|
31
|
+
constec-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
32
|
+
constec-0.2.0.dist-info/top_level.txt,sha256=bQ9AydOLlthShsr7tA7t7ivbLvlLPdhHOo0BdWgnh_Y,8
|
|
33
|
+
constec-0.2.0.dist-info/RECORD,,
|