constec 0.1.0__py3-none-any.whl → 0.2.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.
@@ -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}"
@@ -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.1.0
4
- Summary: Base library for the Constec ecosystem - shared utilities and namespace foundation
3
+ Version: 0.2.1
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,31 @@
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=msXQuh93fquBr7pV3IE1XTInAFQeUeaYan-29h0RTCo,28290
5
+ constec/db/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ constec/db/models/__init__.py,sha256=xsER3dtbsj14mSb52v1Mf1bE0RzKbvrvwD6xti2RN5s,2132
7
+ constec/db/models/base.py,sha256=1t_eYXYyggoL5fKLcitRPKr1qUE1Ql3_sOmSHQMD3QE,359
8
+ constec/db/models/company.py,sha256=VZMt6gmsEWBL6VPUYzTPYOngBqL3mPYmZ6ZuCiz5Jeo,1055
9
+ constec/db/models/contact.py,sha256=qzkekZBNBtusFMpZRANxwKjtDN9qCxc4BMz-H7RyWM0,1904
10
+ constec/db/models/erp.py,sha256=sN0U0XCMNW-pc9CNLZG_kPZeWXznq_XU3ML2zJ3arjA,3518
11
+ constec/db/models/erp_entity.py,sha256=lJG0sU7YfehuxoqrxmpvuCSG0_GsardU5DvwBgMKsW8,3412
12
+ constec/db/models/flow.py,sha256=K_UA7E4zJ2-UPNp5H9e-lfPeGn0wDEiOl0cRal4Dmb8,4414
13
+ constec/db/models/group.py,sha256=ueCOIfpmzR683ojWf5vLb6IG_jawfVRM7IJcGK05I3Y,897
14
+ constec/db/models/module.py,sha256=na_7pJw8Q2D2UkF86ZwK5VdJnV87yqUOMBvaBLw4KgI,1218
15
+ constec/db/models/organization.py,sha256=9dbsh5UBShtDRAeyMqqQEPEaflhzS3aiZpRWUiEluBU,1731
16
+ constec/db/models/person.py,sha256=B4BNdy2AgqmxrZrkWWvqnFxFHs4uO1n61Y_5sP6MsUo,848
17
+ constec/db/models/session.py,sha256=HeIk5rK25IaxKBRX-XNuJ75ndjJZ08rAdSGTLvTwlLE,2695
18
+ constec/db/models/tag.py,sha256=wgJYoffXCO_-G7zhS1FLiQHFtJVbJKPV-wyRG3mwb74,1730
19
+ constec/db/models/user.py,sha256=UzCgb47ZVg1cBN_Y5CIpZJeHSC8JOAYT_0xZE3d294Y,1921
20
+ constec/services/__init__.py,sha256=LXGKIzaRf1gVG2y7oUc18EulUsJXHVs8-oz0nY2h7WA,312
21
+ constec/services/encryption.py,sha256=rwfAGUC7kDy1bhezHPoZG1hTjajpEB6l3AgcfGtmdvg,2368
22
+ constec/shared/__init__.py,sha256=Qe99OxBfwg3_4i4zyCQR20vCcyyaIULpLzUa0XfwBPg,435
23
+ constec/shared/exceptions.py,sha256=8Bih40RWoH0gVhto09mH2ppSQV_drHPnGWITcoD-0J0,1335
24
+ constec/utils/__init__.py,sha256=brf-G4UvU-3CK_rKNPTaHwsVsxnoJSbml_QTZJSM7d0,458
25
+ constec/utils/cuit.py,sha256=dQKGlA4pRQ5DyR-N4BiV8ZsvAle2Vgjif7PU72zHx_A,2753
26
+ constec/utils/password.py,sha256=XNpTJ9xZQSoZNjXEAnexAEZuYkwW1dFgX4AY-B5Q0gA,1462
27
+ constec-0.2.1.dist-info/licenses/LICENSE,sha256=a1R51ONDGq0UQfV-n3ybsNL7EGhcC2sQ1sXvRANaFVI,1064
28
+ constec-0.2.1.dist-info/METADATA,sha256=HOqHTtkL5qv5h6LZK6AV2K9f6i8tTsCKh936Kf8Ge1s,2954
29
+ constec-0.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
30
+ constec-0.2.1.dist-info/top_level.txt,sha256=bQ9AydOLlthShsr7tA7t7ivbLvlLPdhHOo0BdWgnh_Y,8
31
+ constec-0.2.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,7 +0,0 @@
1
- constec/shared/__init__.py,sha256=Qe99OxBfwg3_4i4zyCQR20vCcyyaIULpLzUa0XfwBPg,435
2
- constec/shared/exceptions.py,sha256=8Bih40RWoH0gVhto09mH2ppSQV_drHPnGWITcoD-0J0,1335
3
- constec-0.1.0.dist-info/licenses/LICENSE,sha256=a1R51ONDGq0UQfV-n3ybsNL7EGhcC2sQ1sXvRANaFVI,1064
4
- constec-0.1.0.dist-info/METADATA,sha256=sH68NW_kcBJrOgloDWMCyoy2Wiq3uvqsMUKNyalHtSk,2572
5
- constec-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- constec-0.1.0.dist-info/top_level.txt,sha256=bQ9AydOLlthShsr7tA7t7ivbLvlLPdhHOo0BdWgnh_Y,8
7
- constec-0.1.0.dist-info/RECORD,,