xenfra 0.2.8__py3-none-any.whl → 0.3.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.
@@ -1,234 +1,234 @@
1
- """
2
- Validation utilities for CLI commands.
3
- """
4
-
5
- import re
6
- import uuid
7
- from typing import Optional
8
- from urllib.parse import urlparse
9
-
10
-
11
- def validate_deployment_id(deployment_id: str) -> tuple[bool, Optional[str]]:
12
- """
13
- Validate deployment ID format (UUID or positive integer).
14
-
15
- Returns (is_valid, error_message).
16
- """
17
- if not deployment_id or not isinstance(deployment_id, str):
18
- return False, "Deployment ID cannot be empty"
19
-
20
- deployment_id = deployment_id.strip()
21
-
22
- # Try UUID format first
23
- try:
24
- uuid.UUID(deployment_id)
25
- return True, None
26
- except ValueError:
27
- pass
28
-
29
- # Try positive integer
30
- try:
31
- if int(deployment_id) > 0:
32
- return True, None
33
- except ValueError:
34
- pass
35
-
36
- return False, "Deployment ID must be a valid UUID or positive integer"
37
-
38
-
39
- def validate_project_id(project_id: int) -> tuple[bool, Optional[str]]:
40
- """
41
- Validate project ID (must be positive integer).
42
-
43
- Returns (is_valid, error_message).
44
- """
45
- if not isinstance(project_id, int):
46
- return False, "Project ID must be an integer"
47
-
48
- if project_id <= 0:
49
- return False, "Project ID must be a positive integer"
50
-
51
- return True, None
52
-
53
-
54
- def validate_git_repo_url(git_repo: str) -> tuple[bool, Optional[str]]:
55
- """
56
- Validate git repository URL format.
57
-
58
- Returns (is_valid, error_message).
59
- """
60
- if not git_repo or not isinstance(git_repo, str):
61
- return False, "Git repository URL cannot be empty"
62
-
63
- git_repo = git_repo.strip()
64
-
65
- if len(git_repo) > 2048:
66
- return False, "Git repository URL is too long (max 2048 characters)"
67
-
68
- try:
69
- parsed = urlparse(git_repo)
70
-
71
- # Must have scheme
72
- if parsed.scheme not in ["http", "https", "git"]:
73
- return False, "Git repository URL must use http, https, or git scheme"
74
-
75
- # Must have hostname
76
- if not parsed.hostname:
77
- return False, "Git repository URL must have a hostname"
78
-
79
- # Common git hosting patterns
80
- if not any(
81
- domain in parsed.hostname.lower()
82
- for domain in ["github.com", "gitlab.com", "bitbucket.org", "gitea.com"]
83
- ):
84
- # Allow custom domains but warn
85
- pass
86
-
87
- return True, None
88
-
89
- except Exception as e:
90
- return False, f"Invalid git repository URL format: {e}"
91
-
92
-
93
- def validate_project_name(project_name: str) -> tuple[bool, Optional[str]]:
94
- """
95
- Validate project name format.
96
-
97
- Returns (is_valid, error_message).
98
- """
99
- if not project_name or not isinstance(project_name, str):
100
- return False, "Project name cannot be empty"
101
-
102
- project_name = project_name.strip()
103
-
104
- if len(project_name) > 100:
105
- return False, "Project name is too long (max 100 characters)"
106
-
107
- if len(project_name) < 1:
108
- return False, "Project name is too short (min 1 character)"
109
-
110
- # Alphanumeric, hyphens, underscores, dots
111
- if not re.match(r"^[a-zA-Z0-9._-]+$", project_name):
112
- return (
113
- False,
114
- "Project name can only contain alphanumeric characters, dots, hyphens, and underscores",
115
- )
116
-
117
- # Reserved names
118
- reserved_names = ["admin", "api", "www", "root", "system", "xenfra"]
119
- if project_name.lower() in reserved_names:
120
- return False, f"Project name '{project_name}' is reserved"
121
-
122
- return True, None
123
-
124
-
125
- def validate_branch_name(branch: str) -> tuple[bool, Optional[str]]:
126
- """
127
- Validate git branch name format.
128
-
129
- Returns (is_valid, error_message).
130
- """
131
- if not branch or not isinstance(branch, str):
132
- return False, "Branch name cannot be empty"
133
-
134
- branch = branch.strip()
135
-
136
- if len(branch) > 255:
137
- return False, "Branch name is too long (max 255 characters)"
138
-
139
- # Git branch name rules: no spaces, no special chars except /, -, _
140
- if not re.match(r"^[a-zA-Z0-9/._-]+$", branch):
141
- return (
142
- False,
143
- "Branch name can only contain alphanumeric characters, slashes, dots, hyphens, and underscores",
144
- )
145
-
146
- # Cannot start with . or end with .lock
147
- if branch.startswith(".") or branch.endswith(".lock"):
148
- return False, "Branch name cannot start with '.' or end with '.lock'"
149
-
150
- return True, None
151
-
152
-
153
- def validate_framework(framework: str) -> tuple[bool, Optional[str]]:
154
- """
155
- Validate framework name.
156
-
157
- Returns (is_valid, error_message).
158
- """
159
- if not framework or not isinstance(framework, str):
160
- return False, "Framework cannot be empty"
161
-
162
- framework = framework.strip().lower()
163
-
164
- allowed_frameworks = ["fastapi", "flask", "django", "other"]
165
- if framework not in allowed_frameworks:
166
- return False, f"Framework must be one of: {', '.join(allowed_frameworks)}"
167
-
168
- return True, None
169
-
170
-
171
- def validate_port(port: int) -> tuple[bool, Optional[str]]:
172
- """
173
- Validate port number.
174
-
175
- Returns (is_valid, error_message).
176
- """
177
- if not isinstance(port, int):
178
- return False, "Port must be an integer"
179
-
180
- if port < 1 or port > 65535:
181
- return False, "Port must be between 1 and 65535"
182
-
183
- return True, None
184
-
185
-
186
- def validate_region(region: str) -> tuple[bool, Optional[str]]:
187
- """
188
- Validate DigitalOcean region.
189
-
190
- Returns (is_valid, error_message).
191
- """
192
- if not region or not isinstance(region, str):
193
- return False, "Region cannot be empty"
194
-
195
- region = region.strip().lower()
196
-
197
- # Common DigitalOcean regions (not exhaustive, but validates format)
198
- if not re.match(r"^[a-z]{3}[0-9]$", region):
199
- return False, "Region must be in format 'xxx1' (e.g., 'nyc3', 'sfo3')"
200
-
201
- return True, None
202
-
203
-
204
- def validate_size_slug(size_slug: str) -> tuple[bool, Optional[str]]:
205
- """
206
- Validate DigitalOcean size slug.
207
-
208
- Returns (is_valid, error_message).
209
- """
210
- if not size_slug or not isinstance(size_slug, str):
211
- return False, "Size slug cannot be empty"
212
-
213
- size_slug = size_slug.strip().lower()
214
-
215
- # DigitalOcean size slug format: s-{vcpu}vcpu-{ram}gb
216
- if not re.match(r"^s-[0-9]+vcpu-[0-9]+gb$", size_slug):
217
- return False, "Size slug must be in format 's-Xvcpu-Ygb' (e.g., 's-1vcpu-1gb')"
218
-
219
- return True, None
220
-
221
-
222
- def validate_codebase_scan_limits(max_files: int, max_size: int) -> tuple[bool, Optional[str]]:
223
- """
224
- Validate codebase scan limits.
225
-
226
- Returns (is_valid, error_message).
227
- """
228
- if not isinstance(max_files, int) or max_files < 1 or max_files > 100:
229
- return False, "max_files must be between 1 and 100"
230
-
231
- if not isinstance(max_size, int) or max_size < 1024 or max_size > 10 * 1024 * 1024:
232
- return False, "max_size must be between 1KB and 10MB"
233
-
234
- return True, None
1
+ """
2
+ Validation utilities for CLI commands.
3
+ """
4
+
5
+ import re
6
+ import uuid
7
+ from typing import Optional
8
+ from urllib.parse import urlparse
9
+
10
+
11
+ def validate_deployment_id(deployment_id: str) -> tuple[bool, Optional[str]]:
12
+ """
13
+ Validate deployment ID format (UUID or positive integer).
14
+
15
+ Returns (is_valid, error_message).
16
+ """
17
+ if not deployment_id or not isinstance(deployment_id, str):
18
+ return False, "Deployment ID cannot be empty"
19
+
20
+ deployment_id = deployment_id.strip()
21
+
22
+ # Try UUID format first
23
+ try:
24
+ uuid.UUID(deployment_id)
25
+ return True, None
26
+ except ValueError:
27
+ pass
28
+
29
+ # Try positive integer
30
+ try:
31
+ if int(deployment_id) > 0:
32
+ return True, None
33
+ except ValueError:
34
+ pass
35
+
36
+ return False, "Deployment ID must be a valid UUID or positive integer"
37
+
38
+
39
+ def validate_project_id(project_id: int) -> tuple[bool, Optional[str]]:
40
+ """
41
+ Validate project ID (must be positive integer).
42
+
43
+ Returns (is_valid, error_message).
44
+ """
45
+ if not isinstance(project_id, int):
46
+ return False, "Project ID must be an integer"
47
+
48
+ if project_id <= 0:
49
+ return False, "Project ID must be a positive integer"
50
+
51
+ return True, None
52
+
53
+
54
+ def validate_git_repo_url(git_repo: str) -> tuple[bool, Optional[str]]:
55
+ """
56
+ Validate git repository URL format.
57
+
58
+ Returns (is_valid, error_message).
59
+ """
60
+ if not git_repo or not isinstance(git_repo, str):
61
+ return False, "Git repository URL cannot be empty"
62
+
63
+ git_repo = git_repo.strip()
64
+
65
+ if len(git_repo) > 2048:
66
+ return False, "Git repository URL is too long (max 2048 characters)"
67
+
68
+ try:
69
+ parsed = urlparse(git_repo)
70
+
71
+ # Must have scheme
72
+ if parsed.scheme not in ["http", "https", "git"]:
73
+ return False, "Git repository URL must use http, https, or git scheme"
74
+
75
+ # Must have hostname
76
+ if not parsed.hostname:
77
+ return False, "Git repository URL must have a hostname"
78
+
79
+ # Common git hosting patterns
80
+ if not any(
81
+ domain in parsed.hostname.lower()
82
+ for domain in ["github.com", "gitlab.com", "bitbucket.org", "gitea.com"]
83
+ ):
84
+ # Allow custom domains but warn
85
+ pass
86
+
87
+ return True, None
88
+
89
+ except Exception as e:
90
+ return False, f"Invalid git repository URL format: {e}"
91
+
92
+
93
+ def validate_project_name(project_name: str) -> tuple[bool, Optional[str]]:
94
+ """
95
+ Validate project name format.
96
+
97
+ Returns (is_valid, error_message).
98
+ """
99
+ if not project_name or not isinstance(project_name, str):
100
+ return False, "Project name cannot be empty"
101
+
102
+ project_name = project_name.strip()
103
+
104
+ if len(project_name) > 100:
105
+ return False, "Project name is too long (max 100 characters)"
106
+
107
+ if len(project_name) < 1:
108
+ return False, "Project name is too short (min 1 character)"
109
+
110
+ # Alphanumeric, hyphens, underscores, dots
111
+ if not re.match(r"^[a-zA-Z0-9._-]+$", project_name):
112
+ return (
113
+ False,
114
+ "Project name can only contain alphanumeric characters, dots, hyphens, and underscores",
115
+ )
116
+
117
+ # Reserved names
118
+ reserved_names = ["admin", "api", "www", "root", "system", "xenfra"]
119
+ if project_name.lower() in reserved_names:
120
+ return False, f"Project name '{project_name}' is reserved"
121
+
122
+ return True, None
123
+
124
+
125
+ def validate_branch_name(branch: str) -> tuple[bool, Optional[str]]:
126
+ """
127
+ Validate git branch name format.
128
+
129
+ Returns (is_valid, error_message).
130
+ """
131
+ if not branch or not isinstance(branch, str):
132
+ return False, "Branch name cannot be empty"
133
+
134
+ branch = branch.strip()
135
+
136
+ if len(branch) > 255:
137
+ return False, "Branch name is too long (max 255 characters)"
138
+
139
+ # Git branch name rules: no spaces, no special chars except /, -, _
140
+ if not re.match(r"^[a-zA-Z0-9/._-]+$", branch):
141
+ return (
142
+ False,
143
+ "Branch name can only contain alphanumeric characters, slashes, dots, hyphens, and underscores",
144
+ )
145
+
146
+ # Cannot start with . or end with .lock
147
+ if branch.startswith(".") or branch.endswith(".lock"):
148
+ return False, "Branch name cannot start with '.' or end with '.lock'"
149
+
150
+ return True, None
151
+
152
+
153
+ def validate_framework(framework: str) -> tuple[bool, Optional[str]]:
154
+ """
155
+ Validate framework name.
156
+
157
+ Returns (is_valid, error_message).
158
+ """
159
+ if not framework or not isinstance(framework, str):
160
+ return False, "Framework cannot be empty"
161
+
162
+ framework = framework.strip().lower()
163
+
164
+ allowed_frameworks = ["fastapi", "flask", "django", "other"]
165
+ if framework not in allowed_frameworks:
166
+ return False, f"Framework must be one of: {', '.join(allowed_frameworks)}"
167
+
168
+ return True, None
169
+
170
+
171
+ def validate_port(port: int) -> tuple[bool, Optional[str]]:
172
+ """
173
+ Validate port number.
174
+
175
+ Returns (is_valid, error_message).
176
+ """
177
+ if not isinstance(port, int):
178
+ return False, "Port must be an integer"
179
+
180
+ if port < 1 or port > 65535:
181
+ return False, "Port must be between 1 and 65535"
182
+
183
+ return True, None
184
+
185
+
186
+ def validate_region(region: str) -> tuple[bool, Optional[str]]:
187
+ """
188
+ Validate DigitalOcean region.
189
+
190
+ Returns (is_valid, error_message).
191
+ """
192
+ if not region or not isinstance(region, str):
193
+ return False, "Region cannot be empty"
194
+
195
+ region = region.strip().lower()
196
+
197
+ # Common DigitalOcean regions (not exhaustive, but validates format)
198
+ if not re.match(r"^[a-z]{3}[0-9]$", region):
199
+ return False, "Region must be in format 'xxx1' (e.g., 'nyc3', 'sfo3')"
200
+
201
+ return True, None
202
+
203
+
204
+ def validate_size_slug(size_slug: str) -> tuple[bool, Optional[str]]:
205
+ """
206
+ Validate DigitalOcean size slug.
207
+
208
+ Returns (is_valid, error_message).
209
+ """
210
+ if not size_slug or not isinstance(size_slug, str):
211
+ return False, "Size slug cannot be empty"
212
+
213
+ size_slug = size_slug.strip().lower()
214
+
215
+ # DigitalOcean size slug format: s-{vcpu}vcpu-{ram}gb
216
+ if not re.match(r"^s-[0-9]+vcpu-[0-9]+gb$", size_slug):
217
+ return False, "Size slug must be in format 's-Xvcpu-Ygb' (e.g., 's-1vcpu-1gb')"
218
+
219
+ return True, None
220
+
221
+
222
+ def validate_codebase_scan_limits(max_files: int, max_size: int) -> tuple[bool, Optional[str]]:
223
+ """
224
+ Validate codebase scan limits.
225
+
226
+ Returns (is_valid, error_message).
227
+ """
228
+ if not isinstance(max_files, int) or max_files < 1 or max_files > 100:
229
+ return False, "max_files must be between 1 and 100"
230
+
231
+ if not isinstance(max_size, int) or max_size < 1024 or max_size > 10 * 1024 * 1024:
232
+ return False, "max_size must be between 1KB and 10MB"
233
+
234
+ return True, None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: xenfra
3
- Version: 0.2.8
3
+ Version: 0.3.0
4
4
  Summary: A 'Zen Mode' infrastructure engine for Python developers.
5
5
  Author: xenfra-cloud
6
6
  Author-email: xenfra-cloud <xenfracloud@gmail.com>
@@ -31,86 +31,86 @@ Project-URL: Issues, https://github.com/xenfra-cloud/xenfra/issues
31
31
  Provides-Extra: test
32
32
  Description-Content-Type: text/markdown
33
33
 
34
- # Xenfra CLI
35
-
36
- ## Xenfra CLI: Deploy Python Apps with Zen Mode
37
-
38
- The Xenfra CLI is a powerful and intuitive command-line interface designed to streamline the deployment of Python applications to DigitalOcean. Built with a "Zen Mode" philosophy, it automates complex infrastructure tasks, allowing developers to focus on writing code.
39
-
40
- ### ✨ Key Features
41
-
42
- - **Zero-Configuration Deployment:** Automatically detects your project's framework and dependencies.
43
- - **AI-Powered Auto-Healing:** Diagnoses common deployment failures and suggests, or even applies, fixes automatically.
44
- - **Real-time Monitoring:** View deployment status and stream live application logs directly from your terminal.
45
- - **Integrated Project Management:** Easily list, view, and destroy your deployed projects.
46
- - **Secure Authentication:** Uses OAuth2 PKCE flow for secure, token-based authentication.
47
-
48
- ### 🚀 Quickstart
49
-
50
- #### 1. Installation
51
-
52
- Install the Xenfra CLI using `uv` (recommended) or `pip`:
53
-
54
- ```bash
55
- uv pip install xenfra-cli
56
- # or
57
- pip install xenfra-cli
58
- ```
59
-
60
- #### 2. Authentication
61
-
62
- Log in to your Xenfra account. This will open your web browser to complete the OAuth2 flow.
63
-
64
- ```bash
65
- xenfra auth login
66
- ```
67
-
68
- #### 3. Initialize Your Project
69
-
70
- Navigate to your Python project's root directory and run `init`. The CLI will scan your codebase, detect its characteristics, and generate a `xenfra.yaml` configuration file.
71
-
72
- ```bash
73
- cd your-python-project/
74
- xenfra init
75
- ```
76
-
77
- #### 4. Deploy Your Application
78
-
79
- Once `xenfra.yaml` is configured, deploy your application. The CLI will handle provisioning a DigitalOcean Droplet, setting up Docker, and deploying your code.
80
-
81
- ```bash
82
- xenfra deploy
83
- ```
84
-
85
- ### 📋 Usage Examples
86
-
87
- - **Monitor Deployment Status:**
88
- ```bash
89
- xenfra status <deployment-id>
90
- ```
91
- - **Stream Application Logs:**
92
- ```bash
93
- xenfra logs <deployment-id>
94
- ```
95
- - **List Deployed Projects:**
96
- ```bash
97
- xenfra projects list
98
- ```
99
- - **Diagnose a Failed Deployment (AI-Powered):**
100
- ```bash
101
- xenfra diagnose <deployment-id>
102
- # Or to diagnose from a log file:
103
- xenfra diagnose --logs error.log
104
- ```
105
-
106
- ### 📚 Documentation
107
-
108
- For more detailed information, advanced configurations, and API references, please refer to the [official Xenfra Documentation](https://docs.xenfra.tech/cli) (Link will be updated upon final deployment).
109
-
110
- ### 🤝 Contributing
111
-
112
- We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more details.
113
-
114
- ### 📄 License
115
-
116
- This project is licensed under the [MIT License](LICENSE).
34
+ # Xenfra CLI
35
+
36
+ ## Xenfra CLI: Deploy Python Apps with Zen Mode
37
+
38
+ The Xenfra CLI is a powerful and intuitive command-line interface designed to streamline the deployment of Python applications to DigitalOcean. Built with a "Zen Mode" philosophy, it automates complex infrastructure tasks, allowing developers to focus on writing code.
39
+
40
+ ### ✨ Key Features
41
+
42
+ - **Zero-Configuration Deployment:** Automatically detects your project's framework and dependencies.
43
+ - **AI-Powered Auto-Healing:** Diagnoses common deployment failures and suggests, or even applies, fixes automatically.
44
+ - **Real-time Monitoring:** View deployment status and stream live application logs directly from your terminal.
45
+ - **Integrated Project Management:** Easily list, view, and destroy your deployed projects.
46
+ - **Secure Authentication:** Uses OAuth2 PKCE flow for secure, token-based authentication.
47
+
48
+ ### 🚀 Quickstart
49
+
50
+ #### 1. Installation
51
+
52
+ Install the Xenfra CLI using `uv` (recommended) or `pip`:
53
+
54
+ ```bash
55
+ uv pip install xenfra-cli
56
+ # or
57
+ pip install xenfra-cli
58
+ ```
59
+
60
+ #### 2. Authentication
61
+
62
+ Log in to your Xenfra account. This will open your web browser to complete the OAuth2 flow.
63
+
64
+ ```bash
65
+ xenfra auth login
66
+ ```
67
+
68
+ #### 3. Initialize Your Project
69
+
70
+ Navigate to your Python project's root directory and run `init`. The CLI will scan your codebase, detect its characteristics, and generate a `xenfra.yaml` configuration file.
71
+
72
+ ```bash
73
+ cd your-python-project/
74
+ xenfra init
75
+ ```
76
+
77
+ #### 4. Deploy Your Application
78
+
79
+ Once `xenfra.yaml` is configured, deploy your application. The CLI will handle provisioning a DigitalOcean Droplet, setting up Docker, and deploying your code.
80
+
81
+ ```bash
82
+ xenfra deploy
83
+ ```
84
+
85
+ ### 📋 Usage Examples
86
+
87
+ - **Monitor Deployment Status:**
88
+ ```bash
89
+ xenfra status <deployment-id>
90
+ ```
91
+ - **Stream Application Logs:**
92
+ ```bash
93
+ xenfra logs <deployment-id>
94
+ ```
95
+ - **List Deployed Projects:**
96
+ ```bash
97
+ xenfra projects list
98
+ ```
99
+ - **Diagnose a Failed Deployment (AI-Powered):**
100
+ ```bash
101
+ xenfra diagnose <deployment-id>
102
+ # Or to diagnose from a log file:
103
+ xenfra diagnose --logs error.log
104
+ ```
105
+
106
+ ### 📚 Documentation
107
+
108
+ For more detailed information, advanced configurations, and API references, please refer to the [official Xenfra Documentation](https://docs.xenfra.tech/cli) (Link will be updated upon final deployment).
109
+
110
+ ### 🤝 Contributing
111
+
112
+ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more details.
113
+
114
+ ### 📄 License
115
+
116
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,19 @@
1
+ xenfra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ xenfra/commands/__init__.py,sha256=kTTwVnTvoxikyPUhQiyTAbnw4PYafktuE1----TqQoA,43
3
+ xenfra/commands/auth.py,sha256=UMtaIPsBqrVoYDmXkbQZoeEQrPyf6vHQmgPFS4PGwYQ,4109
4
+ xenfra/commands/auth_device.py,sha256=BfSjyoOHgvfPH69QdD4fF_VdCktS99z2k7UGFPClWls,6364
5
+ xenfra/commands/deployments.py,sha256=bI6d9lVYPhCDoQE3nke7s80MUQ75ILPqiEGhcbXDExo,28609
6
+ xenfra/commands/intelligence.py,sha256=1lw1Pw6deyC52JmpK8YAVFuydAC7cvg_2qiRnw8B8Yc,13884
7
+ xenfra/commands/projects.py,sha256=SAxF_pOr95K6uz35U-zENptKndKxJNZn6bcD45PHcpI,6696
8
+ xenfra/commands/security_cmd.py,sha256=EI5sjX2lcMxgMH-LCFmPVkc9YqadOrcoSgTiKknkVRY,7327
9
+ xenfra/main.py,sha256=2EPPuIdxjhW-I-e-Mc0i2ayeLaSJdmzddNThkXq7B7c,2033
10
+ xenfra/utils/__init__.py,sha256=4ZRYkrb--vzoXjBHG8zRxz2jCXNGtAoKNtkyu2WRI2A,45
11
+ xenfra/utils/auth.py,sha256=L6fDwYbvDhZ7gky68GxNEMHGxdi-k1kzFvjDSe-uqI4,9092
12
+ xenfra/utils/codebase.py,sha256=57GthXOvOQnUHiDwIHqxK6hNUGWlWf6Nfs3T8647Wrc,4144
13
+ xenfra/utils/config.py,sha256=F2zedd3JXP7TBdul0u8b4NVx-C1N6Hq4sH5szyWim6M,11947
14
+ xenfra/utils/security.py,sha256=EA8CIPLt8Y-QP5uZ7c5NuC6ZLRV1aZS8NapS9ix_vok,11479
15
+ xenfra/utils/validation.py,sha256=cvuL_AEFJ2oCoP0abCqoOIABOwz79Gkf-jh_dcFIQlM,6912
16
+ xenfra-0.3.0.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
17
+ xenfra-0.3.0.dist-info/entry_points.txt,sha256=a_2cGhYK__X6eW05Ba8uB6RIM_61c2sHtXsPY8N0mic,45
18
+ xenfra-0.3.0.dist-info/METADATA,sha256=kA5doOVqpnopHTMwGnBbOS8CJ8xhg77_prwocC609j8,3834
19
+ xenfra-0.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.18
2
+ Generator: uv 0.9.21
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any