xenfra-sdk 0.1.1__py3-none-any.whl → 0.1.3__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.
xenfra_sdk/privacy.py CHANGED
@@ -1,151 +1,153 @@
1
- """
2
- This module contains the Privacy Scrubber for the Xenfra SDK.
3
- Its purpose is to redact sensitive information from logs or other text
4
- before it is sent to diagnostic endpoints, upholding privacy-first principles.
5
- """
6
-
7
- import json
8
- import logging
9
- import os
10
- import re
11
- from pathlib import Path
12
- from typing import List, Optional
13
-
14
- import httpx # For fetching patterns from URL
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
- # Path to the patterns file within the SDK
19
- _PATTERNS_FILE_PATH = Path(__file__).parent / "patterns.json"
20
- _REDACTION_PLACEHOLDER = "[REDACTED]"
21
- _CACHED_PATTERNS: List[re.Pattern] = []
22
-
23
-
24
- def _load_patterns_from_file(file_path: Path) -> List[str]:
25
- """Loads raw regex patterns from a JSON file."""
26
- if not file_path.exists():
27
- logger.warning(
28
- f"Patterns file not found at {file_path}. No patterns will be used for scrubbing."
29
- )
30
- return []
31
- try:
32
- with open(file_path, "r") as f:
33
- config = json.load(f)
34
- return config.get("redaction_patterns", [])
35
- except json.JSONDecodeError as e:
36
- logger.error(f"Error decoding patterns.json: {e}. Falling back to empty patterns.")
37
- return []
38
-
39
-
40
- async def _refresh_patterns_from_url(url: str) -> Optional[List[str]]:
41
- """
42
- Fetches updated patterns from a URL asynchronously.
43
- """
44
- try:
45
- # Configure timeout from environment or default to 30 seconds
46
- timeout_seconds = float(os.getenv("XENFRA_SDK_TIMEOUT", "30.0"))
47
- timeout = httpx.Timeout(timeout_seconds, connect=10.0)
48
-
49
- async with httpx.AsyncClient(timeout=timeout) as client:
50
- response = await client.get(url)
51
- response.raise_for_status()
52
-
53
- # Safe JSON parsing with content-type check
54
- content_type = response.headers.get("content-type", "")
55
- if "application/json" not in content_type:
56
- logger.warning(
57
- f"Expected JSON response from {url}, got {content_type}. "
58
- "Skipping pattern refresh."
59
- )
60
- return None
61
-
62
- try:
63
- config = response.json()
64
- except (ValueError, TypeError) as e:
65
- logger.error(f"Failed to parse JSON from patterns URL {url}: {e}")
66
- return None
67
-
68
- if not isinstance(config, dict):
69
- logger.error(f"Expected dictionary from patterns URL {url}, got {type(config).__name__}")
70
- return None
71
-
72
- return config.get("redaction_patterns", [])
73
- except httpx.TimeoutException as e:
74
- logger.warning(f"Timeout fetching patterns from {url}: {e}")
75
- return None
76
- except httpx.RequestError as e:
77
- logger.warning(f"Error fetching patterns from {url}: {e}")
78
- return None
79
- except json.JSONDecodeError as e:
80
- logger.error(f"Error decoding JSON from patterns URL {url}: {e}")
81
- return None
82
- except Exception as e:
83
- logger.error(f"Unexpected error fetching patterns from {url}: {e}")
84
- return None
85
-
86
-
87
- async def initialize_scrubber(refresh_from_url: Optional[str] = None):
88
- """
89
- Initializes or refreshes the scrubber patterns.
90
- Can optionally fetch patterns from a URL. This should be called on app startup.
91
- """
92
- global _CACHED_PATTERNS
93
- raw_patterns = []
94
-
95
- if refresh_from_url:
96
- refreshed = await _refresh_patterns_from_url(refresh_from_url)
97
- if refreshed:
98
- raw_patterns = refreshed
99
-
100
- if not raw_patterns: # Fallback to file if no refresh URL or refresh failed
101
- raw_patterns = _load_patterns_from_file(_PATTERNS_FILE_PATH)
102
-
103
- _CACHED_PATTERNS = [re.compile(p, re.IGNORECASE) for p in raw_patterns]
104
-
105
-
106
- # Initialize patterns on module load (synchronously for initial load)
107
- # For dynamic refresh, initialize_scrubber should be called during app startup
108
- _raw_initial_patterns = _load_patterns_from_file(_PATTERNS_FILE_PATH)
109
- _CACHED_PATTERNS = [re.compile(p, re.IGNORECASE) for p in _raw_initial_patterns]
110
-
111
-
112
- def scrub_logs(logs: str) -> str:
113
- """
114
- Redacts sensitive information from log strings using loaded patterns.
115
- """
116
- if not logs:
117
- return logs
118
-
119
- scrubbed_logs = logs
120
- for pattern_re in _CACHED_PATTERNS:
121
- scrubbed_logs = pattern_re.sub(_REDACTION_PLACEHOLDER, scrubbed_logs)
122
-
123
- return scrubbed_logs
124
-
125
-
126
- if __name__ == "__main__":
127
- # Example Usage
128
- test_logs = """
129
- Deployment failed. Error: Authentication failed with token dop_v1_abcdefghijklmnopqrstuvwxyz1234567890abcdef.
130
- Connecting to database at postgres://user:mypassword@127.0.0.1:5432/mydb.
131
- Received request from 192.168.1.100. User: test@example.com.
132
- Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
133
- eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
134
- SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c.
135
- AWS Secret: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY.
136
- """
137
-
138
- # Test with file-based patterns
139
- print("--- Original Logs ---")
140
- print(test_logs)
141
- print("\n--- Scrubbed Logs (from file) ---")
142
- scrubbed_logs_from_file = scrub_logs(test_logs)
143
- print(scrubbed_logs_from_file)
144
-
145
- # Example of refreshing (conceptual)
146
- # import asyncio
147
- # async def demo_refresh():
148
- # await initialize_scrubber(refresh_from_url="http://example.com/new-patterns.json")
149
- # print("\n--- Scrubbed Logs (after conceptual refresh) ---")
150
- # print(scrub_logs(test_logs))
151
- # asyncio.run(demo_refresh())
1
+ """
2
+ This module contains the Privacy Scrubber for the Xenfra SDK.
3
+ Its purpose is to redact sensitive information from logs or other text
4
+ before it is sent to diagnostic endpoints, upholding privacy-first principles.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ import re
11
+ from pathlib import Path
12
+ from typing import List, Optional
13
+
14
+ import httpx # For fetching patterns from URL
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Path to the patterns file within the SDK
19
+ _PATTERNS_FILE_PATH = Path(__file__).parent / "patterns.json"
20
+ _REDACTION_PLACEHOLDER = "[REDACTED]"
21
+ _CACHED_PATTERNS: List[re.Pattern] = []
22
+
23
+
24
+ def _load_patterns_from_file(file_path: Path) -> List[str]:
25
+ """Loads raw regex patterns from a JSON file."""
26
+ if not file_path.exists():
27
+ logger.warning(
28
+ f"Patterns file not found at {file_path}. No patterns will be used for scrubbing."
29
+ )
30
+ return []
31
+ try:
32
+ with open(file_path, "r") as f:
33
+ config = json.load(f)
34
+ return config.get("redaction_patterns", [])
35
+ except json.JSONDecodeError as e:
36
+ logger.error(f"Error decoding patterns.json: {e}. Falling back to empty patterns.")
37
+ return []
38
+
39
+
40
+ async def _refresh_patterns_from_url(url: str) -> Optional[List[str]]:
41
+ """
42
+ Fetches updated patterns from a URL asynchronously.
43
+ """
44
+ try:
45
+ # Configure timeout from environment or default to 30 seconds
46
+ timeout_seconds = float(os.getenv("XENFRA_SDK_TIMEOUT", "30.0"))
47
+ timeout = httpx.Timeout(timeout_seconds, connect=10.0)
48
+
49
+ async with httpx.AsyncClient(timeout=timeout) as client:
50
+ response = await client.get(url)
51
+ response.raise_for_status()
52
+
53
+ # Safe JSON parsing with content-type check
54
+ content_type = response.headers.get("content-type", "")
55
+ if "application/json" not in content_type:
56
+ logger.warning(
57
+ f"Expected JSON response from {url}, got {content_type}. "
58
+ "Skipping pattern refresh."
59
+ )
60
+ return None
61
+
62
+ try:
63
+ config = response.json()
64
+ except (ValueError, TypeError) as e:
65
+ logger.error(f"Failed to parse JSON from patterns URL {url}: {e}")
66
+ return None
67
+
68
+ if not isinstance(config, dict):
69
+ logger.error(
70
+ f"Expected dictionary from patterns URL {url}, got {type(config).__name__}"
71
+ )
72
+ return None
73
+
74
+ return config.get("redaction_patterns", [])
75
+ except httpx.TimeoutException as e:
76
+ logger.warning(f"Timeout fetching patterns from {url}: {e}")
77
+ return None
78
+ except httpx.RequestError as e:
79
+ logger.warning(f"Error fetching patterns from {url}: {e}")
80
+ return None
81
+ except json.JSONDecodeError as e:
82
+ logger.error(f"Error decoding JSON from patterns URL {url}: {e}")
83
+ return None
84
+ except Exception as e:
85
+ logger.error(f"Unexpected error fetching patterns from {url}: {e}")
86
+ return None
87
+
88
+
89
+ async def initialize_scrubber(refresh_from_url: Optional[str] = None):
90
+ """
91
+ Initializes or refreshes the scrubber patterns.
92
+ Can optionally fetch patterns from a URL. This should be called on app startup.
93
+ """
94
+ global _CACHED_PATTERNS
95
+ raw_patterns = []
96
+
97
+ if refresh_from_url:
98
+ refreshed = await _refresh_patterns_from_url(refresh_from_url)
99
+ if refreshed:
100
+ raw_patterns = refreshed
101
+
102
+ if not raw_patterns: # Fallback to file if no refresh URL or refresh failed
103
+ raw_patterns = _load_patterns_from_file(_PATTERNS_FILE_PATH)
104
+
105
+ _CACHED_PATTERNS = [re.compile(p, re.IGNORECASE) for p in raw_patterns]
106
+
107
+
108
+ # Initialize patterns on module load (synchronously for initial load)
109
+ # For dynamic refresh, initialize_scrubber should be called during app startup
110
+ _raw_initial_patterns = _load_patterns_from_file(_PATTERNS_FILE_PATH)
111
+ _CACHED_PATTERNS = [re.compile(p, re.IGNORECASE) for p in _raw_initial_patterns]
112
+
113
+
114
+ def scrub_logs(logs: str) -> str:
115
+ """
116
+ Redacts sensitive information from log strings using loaded patterns.
117
+ """
118
+ if not logs:
119
+ return logs
120
+
121
+ scrubbed_logs = logs
122
+ for pattern_re in _CACHED_PATTERNS:
123
+ scrubbed_logs = pattern_re.sub(_REDACTION_PLACEHOLDER, scrubbed_logs)
124
+
125
+ return scrubbed_logs
126
+
127
+
128
+ if __name__ == "__main__":
129
+ # Example Usage
130
+ test_logs = """
131
+ Deployment failed. Error: Authentication failed with token dop_v1_abcdefghijklmnopqrstuvwxyz1234567890abcdef.
132
+ Connecting to database at postgres://user:mypassword@127.0.0.1:5432/mydb.
133
+ Received request from 192.168.1.100. User: test@example.com.
134
+ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
135
+ eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
136
+ SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c.
137
+ AWS Secret: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY.
138
+ """
139
+
140
+ # Test with file-based patterns
141
+ print("--- Original Logs ---")
142
+ print(test_logs)
143
+ print("\n--- Scrubbed Logs (from file) ---")
144
+ scrubbed_logs_from_file = scrub_logs(test_logs)
145
+ print(scrubbed_logs_from_file)
146
+
147
+ # Example of refreshing (conceptual)
148
+ # import asyncio
149
+ # async def demo_refresh():
150
+ # await initialize_scrubber(refresh_from_url="http://example.com/new-patterns.json")
151
+ # print("\n--- Scrubbed Logs (after conceptual refresh) ---")
152
+ # print(scrub_logs(test_logs))
153
+ # asyncio.run(demo_refresh())
xenfra_sdk/recipes.py CHANGED
@@ -1,25 +1,25 @@
1
- from pathlib import Path
2
-
3
- from jinja2 import Environment, FileSystemLoader
4
-
5
-
6
- def generate_stack(context: dict):
7
- """
8
- Generates a cloud-init startup script from a Jinja2 template.
9
-
10
- Args:
11
- context: A dictionary containing information for rendering the template,
12
- e.g., {'domain': 'example.com', 'email': 'user@example.com'}
13
- """
14
- # Path to the templates directory
15
- template_dir = Path(__file__).parent / "templates"
16
- env = Environment(loader=FileSystemLoader(template_dir))
17
-
18
- template = env.get_template("cloud-init.sh.j2")
19
-
20
- # The non-dockerized logic has been removed as we are focusing on
21
- # a purely Docker-based deployment strategy for simplicity and scalability.
22
- # The context will contain all necessary variables for the template.
23
- script = template.render(context)
24
-
25
- return script
1
+ from pathlib import Path
2
+
3
+ from jinja2 import Environment, FileSystemLoader
4
+
5
+
6
+ def generate_stack(context: dict):
7
+ """
8
+ Generates a cloud-init startup script from a Jinja2 template.
9
+
10
+ Args:
11
+ context: A dictionary containing information for rendering the template,
12
+ e.g., {'domain': 'example.com', 'email': 'user@example.com'}
13
+ """
14
+ # Path to the templates directory
15
+ template_dir = Path(__file__).parent / "templates"
16
+ env = Environment(loader=FileSystemLoader(template_dir))
17
+
18
+ template = env.get_template("cloud-init.sh.j2")
19
+
20
+ # The non-dockerized logic has been removed as we are focusing on
21
+ # a purely Docker-based deployment strategy for simplicity and scalability.
22
+ # The context will contain all necessary variables for the template.
23
+ script = template.render(context)
24
+
25
+ return script
@@ -1,3 +1,3 @@
1
- class BaseManager:
2
- def __init__(self, client):
3
- self._client = client
1
+ class BaseManager:
2
+ def __init__(self, client):
3
+ self._client = client
@@ -1,85 +1,89 @@
1
- import logging
2
-
3
- # Import Deployment model when it's defined in models.py
4
- # from ..models import Deployment
5
- from ..exceptions import XenfraAPIError, XenfraError # Add XenfraError
6
- from ..utils import safe_get_json_field, safe_json_parse
7
- from .base import BaseManager
8
-
9
- logger = logging.getLogger(__name__)
10
-
11
-
12
- class DeploymentsManager(BaseManager):
13
- def create(self, project_name: str, git_repo: str, branch: str, framework: str) -> dict:
14
- """Creates a new deployment."""
15
- try:
16
- payload = {
17
- "project_name": project_name,
18
- "git_repo": git_repo,
19
- "branch": branch,
20
- "framework": framework,
21
- }
22
- response = self._client._request("POST", "/deployments", json=payload)
23
- # Safe JSON parsing
24
- return safe_json_parse(response)
25
- except XenfraAPIError:
26
- raise
27
- except Exception as e:
28
- raise XenfraError(f"Failed to create deployment: {e}")
29
-
30
- def get_status(self, deployment_id: str) -> dict:
31
- """Get status for a specific deployment.
32
-
33
- Args:
34
- deployment_id: The unique identifier for the deployment.
35
-
36
- Returns:
37
- dict: Deployment status information including state, progress, etc.
38
-
39
- Raises:
40
- XenfraAPIError: If the API returns an error (e.g., 404 not found).
41
- XenfraError: If there's a network or parsing error.
42
- """
43
- try:
44
- response = self._client._request("GET", f"/deployments/{deployment_id}/status")
45
- logger.debug(f"DeploymentsManager.get_status({deployment_id}) response: {response.status_code}")
46
- # Safe JSON parsing - _request() already handles status codes
47
- return safe_json_parse(response)
48
- except XenfraAPIError:
49
- raise # Re-raise API errors
50
- except Exception as e:
51
- raise XenfraError(f"Failed to get status for deployment {deployment_id}: {e}")
52
-
53
- def get_logs(self, deployment_id: str) -> str:
54
- """Get logs for a specific deployment.
55
-
56
- Args:
57
- deployment_id: The unique identifier for the deployment.
58
-
59
- Returns:
60
- str: The deployment logs as plain text.
61
-
62
- Raises:
63
- XenfraAPIError: If the API returns an error (e.g., 404 not found).
64
- XenfraError: If there's a network or parsing error.
65
- """
66
- try:
67
- response = self._client._request("GET", f"/deployments/{deployment_id}/logs")
68
- logger.debug(f"DeploymentsManager.get_logs({deployment_id}) response: {response.status_code}")
69
-
70
- # Safe JSON parsing with structure validation - _request() already handles status codes
71
- data = safe_json_parse(response)
72
- if not isinstance(data, dict):
73
- raise XenfraError(f"Expected dictionary response, got {type(data).__name__}")
74
-
75
- logs = safe_get_json_field(data, "logs", "")
76
-
77
- if not logs:
78
- logger.warning(f"No logs found for deployment {deployment_id}")
79
-
80
- return logs
81
-
82
- except XenfraAPIError:
83
- raise # Re-raise API errors
84
- except Exception as e:
85
- raise XenfraError(f"Failed to get logs for deployment {deployment_id}: {e}")
1
+ import logging
2
+
3
+ # Import Deployment model when it's defined in models.py
4
+ # from ..models import Deployment
5
+ from ..exceptions import XenfraAPIError, XenfraError # Add XenfraError
6
+ from ..utils import safe_get_json_field, safe_json_parse
7
+ from .base import BaseManager
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class DeploymentsManager(BaseManager):
13
+ def create(self, project_name: str, git_repo: str, branch: str, framework: str) -> dict:
14
+ """Creates a new deployment."""
15
+ try:
16
+ payload = {
17
+ "project_name": project_name,
18
+ "git_repo": git_repo,
19
+ "branch": branch,
20
+ "framework": framework,
21
+ }
22
+ response = self._client._request("POST", "/deployments", json=payload)
23
+ # Safe JSON parsing
24
+ return safe_json_parse(response)
25
+ except XenfraAPIError:
26
+ raise
27
+ except Exception as e:
28
+ raise XenfraError(f"Failed to create deployment: {e}")
29
+
30
+ def get_status(self, deployment_id: str) -> dict:
31
+ """Get status for a specific deployment.
32
+
33
+ Args:
34
+ deployment_id: The unique identifier for the deployment.
35
+
36
+ Returns:
37
+ dict: Deployment status information including state, progress, etc.
38
+
39
+ Raises:
40
+ XenfraAPIError: If the API returns an error (e.g., 404 not found).
41
+ XenfraError: If there's a network or parsing error.
42
+ """
43
+ try:
44
+ response = self._client._request("GET", f"/deployments/{deployment_id}/status")
45
+ logger.debug(
46
+ f"DeploymentsManager.get_status({deployment_id}) response: {response.status_code}"
47
+ )
48
+ # Safe JSON parsing - _request() already handles status codes
49
+ return safe_json_parse(response)
50
+ except XenfraAPIError:
51
+ raise # Re-raise API errors
52
+ except Exception as e:
53
+ raise XenfraError(f"Failed to get status for deployment {deployment_id}: {e}")
54
+
55
+ def get_logs(self, deployment_id: str) -> str:
56
+ """Get logs for a specific deployment.
57
+
58
+ Args:
59
+ deployment_id: The unique identifier for the deployment.
60
+
61
+ Returns:
62
+ str: The deployment logs as plain text.
63
+
64
+ Raises:
65
+ XenfraAPIError: If the API returns an error (e.g., 404 not found).
66
+ XenfraError: If there's a network or parsing error.
67
+ """
68
+ try:
69
+ response = self._client._request("GET", f"/deployments/{deployment_id}/logs")
70
+ logger.debug(
71
+ f"DeploymentsManager.get_logs({deployment_id}) response: {response.status_code}"
72
+ )
73
+
74
+ # Safe JSON parsing with structure validation - _request() already handles status codes
75
+ data = safe_json_parse(response)
76
+ if not isinstance(data, dict):
77
+ raise XenfraError(f"Expected dictionary response, got {type(data).__name__}")
78
+
79
+ logs = safe_get_json_field(data, "logs", "")
80
+
81
+ if not logs:
82
+ logger.warning(f"No logs found for deployment {deployment_id}")
83
+
84
+ return logs
85
+
86
+ except XenfraAPIError:
87
+ raise # Re-raise API errors
88
+ except Exception as e:
89
+ raise XenfraError(f"Failed to get logs for deployment {deployment_id}: {e}")