protegrity-ai-developer-python 1.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.
Files changed (53) hide show
  1. appython/__init__.py +12 -0
  2. appython/protector.py +554 -0
  3. appython/service/auth_provider.py +273 -0
  4. appython/service/auth_token_provider.py +45 -0
  5. appython/service/config.py +209 -0
  6. appython/service/payload_builder.py +141 -0
  7. appython/service/request_handler.py +115 -0
  8. appython/service/response_handler.py +78 -0
  9. appython/stats/__init__.py +3 -0
  10. appython/stats/collector.py +90 -0
  11. appython/stats/writer.py +185 -0
  12. appython/utils/codec_helper.py +86 -0
  13. appython/utils/constants.py +246 -0
  14. appython/utils/exceptions.py +141 -0
  15. appython/utils/input_preprocessor.py +325 -0
  16. appython/utils/output_postprocessor.py +99 -0
  17. protegrity_ai_developer_python-1.2.1.dist-info/METADATA +428 -0
  18. protegrity_ai_developer_python-1.2.1.dist-info/RECORD +53 -0
  19. protegrity_ai_developer_python-1.2.1.dist-info/WHEEL +5 -0
  20. protegrity_ai_developer_python-1.2.1.dist-info/entry_points.txt +2 -0
  21. protegrity_ai_developer_python-1.2.1.dist-info/licenses/LICENSE +21 -0
  22. protegrity_ai_developer_python-1.2.1.dist-info/top_level.txt +3 -0
  23. protegrity_developer_python/__init__.py +4 -0
  24. protegrity_developer_python/scan.py +37 -0
  25. protegrity_developer_python/securefind.py +83 -0
  26. protegrity_developer_python/utils/ccn_processing.py +59 -0
  27. protegrity_developer_python/utils/config.py +60 -0
  28. protegrity_developer_python/utils/constants.py +123 -0
  29. protegrity_developer_python/utils/discover.py +49 -0
  30. protegrity_developer_python/utils/logger.py +23 -0
  31. protegrity_developer_python/utils/pii_processing.py +291 -0
  32. protegrity_developer_python/utils/protector.py +23 -0
  33. protegrity_developer_python/utils/semantic_guardrails.py +240 -0
  34. protegrity_developer_python/utils/transform.py +66 -0
  35. pty_migrate/__init__.py +1 -0
  36. pty_migrate/check_cmd.py +871 -0
  37. pty_migrate/cli.py +93 -0
  38. pty_migrate/config.py +127 -0
  39. pty_migrate/create_policy_cmd.py +795 -0
  40. pty_migrate/payloads/__init__.py +51 -0
  41. pty_migrate/payloads/alphabets.json +42 -0
  42. pty_migrate/payloads/dataelements.json +342 -0
  43. pty_migrate/payloads/datastores.json +7 -0
  44. pty_migrate/payloads/deploy_policy_ta.json +1 -0
  45. pty_migrate/payloads/masks.json +18 -0
  46. pty_migrate/payloads/members.json +62 -0
  47. pty_migrate/payloads/policies.json +13 -0
  48. pty_migrate/payloads/roles.json +32 -0
  49. pty_migrate/payloads/rules.json +1639 -0
  50. pty_migrate/payloads/sources.json +10 -0
  51. pty_migrate/payloads/trusted_apps.json +8 -0
  52. pty_migrate/ppc_client.py +371 -0
  53. pty_migrate/stats_cmd.py +87 -0
@@ -0,0 +1,115 @@
1
+ """
2
+ This module provides the RequestHandler class, which is responsible for sending HTTP requests to a specified API
3
+ endpoint using the `requests` library. It includes methods to send POST requests with JSON payloads and handle
4
+ the necessary headers for authentication.
5
+ """
6
+
7
+ import requests
8
+ from requests.adapters import HTTPAdapter
9
+ from urllib3.util.retry import Retry
10
+
11
+
12
+ _DEFAULT_TIMEOUT = 30
13
+ _DEFAULT_MAX_RETRIES = 3
14
+
15
+ # One Session per (max_retries, backoff) combination so a connection pool can be
16
+ # reused across calls without re-mounting adapters on every request.
17
+ _session_cache: dict = {}
18
+
19
+
20
+ def _get_session(max_retries: int) -> requests.Session:
21
+ sess = _session_cache.get(max_retries)
22
+ if sess is not None:
23
+ return sess
24
+ sess = requests.Session()
25
+ if max_retries > 0:
26
+ retry = Retry(
27
+ total=max_retries,
28
+ connect=max_retries,
29
+ read=max_retries,
30
+ status=max_retries,
31
+ status_forcelist=(429, 500, 502, 503, 504),
32
+ allowed_methods=frozenset(["GET", "POST", "PUT", "DELETE"]),
33
+ backoff_factor=0.5, # 0.5s, 1s, 2s, 4s, ...
34
+ respect_retry_after_header=True,
35
+ raise_on_status=False,
36
+ )
37
+ adapter = HTTPAdapter(max_retries=retry)
38
+ sess.mount("https://", adapter)
39
+ sess.mount("http://", adapter)
40
+ _session_cache[max_retries] = sess
41
+ return sess
42
+
43
+
44
+ def _resolve_resilience(config):
45
+ """Pull timeout (seconds) and max_retries from an SDK config dict."""
46
+ if not config:
47
+ return _DEFAULT_TIMEOUT, _DEFAULT_MAX_RETRIES
48
+ try:
49
+ timeout = int(config.get("request_timeout", _DEFAULT_TIMEOUT))
50
+ except (TypeError, ValueError):
51
+ timeout = _DEFAULT_TIMEOUT
52
+ try:
53
+ retries = int(config.get("max_retries", _DEFAULT_MAX_RETRIES))
54
+ except (TypeError, ValueError):
55
+ retries = _DEFAULT_MAX_RETRIES
56
+ return max(1, timeout), max(0, retries)
57
+
58
+
59
+ class RequestHandler:
60
+ def send_api_request(
61
+ payload: dict, base_url: str, api_key: str, jwt_token: str
62
+ ) -> requests.Response:
63
+ """
64
+ Sends a POST request to the specified API endpoint with a JSON payload.
65
+ Legacy method retained for backward compatibility.
66
+
67
+ Parameters:
68
+ payload (dict): The JSON-serializable payload to send in the request body.
69
+ base_url (str): The full URL of the API endpoint to which the request is sent.
70
+ api_key (str): API key for x-api-key header.
71
+ jwt_token (str): JWT token for Authorization header.
72
+
73
+ Returns:
74
+ Response: The HTTP response object returned by the `requests.post` call.
75
+ """
76
+ headers = {
77
+ "Content-Type": "application/json",
78
+ "x-api-key": api_key,
79
+ "Authorization": jwt_token,
80
+ }
81
+ # Legacy path: apply default timeout but no retries (preserves caller
82
+ # behavior for the older DE auth flow).
83
+ response = requests.post(
84
+ base_url, json=payload, headers=headers, timeout=_DEFAULT_TIMEOUT
85
+ )
86
+ return response
87
+
88
+ def send_request(
89
+ payload: dict, url: str, auth_provider, config: dict = None
90
+ ) -> requests.Response:
91
+ """
92
+ Sends a POST request using the pluggable auth provider for authentication.
93
+
94
+ Parameters:
95
+ payload (dict): The JSON-serializable payload to send in the request body.
96
+ url (str): The full URL of the API endpoint.
97
+ auth_provider: An AuthProvider instance that signs the request.
98
+ config (dict, optional): SDK config dict. Honors keys:
99
+ - "request_timeout" (seconds, default 30) — per-attempt HTTP timeout.
100
+ - "max_retries" (int, default 3) — retries on connection errors and
101
+ HTTP 429/5xx with exponential backoff and jitter. 0 disables retries.
102
+
103
+ Returns:
104
+ Response: The HTTP response object returned by the `requests.post` call.
105
+ """
106
+ timeout, max_retries = _resolve_resilience(config)
107
+ headers = {"Content-Type": "application/json"}
108
+ headers = auth_provider.authenticate_request("POST", url, headers, payload)
109
+ kwargs = auth_provider.get_request_kwargs()
110
+ session = _get_session(max_retries)
111
+ response = session.post(
112
+ url, json=payload, headers=headers, timeout=timeout, **kwargs
113
+ )
114
+ return response
115
+
@@ -0,0 +1,78 @@
1
+ """
2
+ This module provides the ResponseHandler class, which processes HTTP responses from the API.
3
+ """
4
+
5
+ from appython.utils.output_postprocessor import OutputProcessor
6
+ from appython.utils.constants import (
7
+ RETURN_CODE as return_code,
8
+ LOG_RETURN_CODE_UNSUPPORTED as log_return_code
9
+ )
10
+ from appython.utils.exceptions import PythonSDKException
11
+
12
+
13
+ class ResponseHandler:
14
+ @staticmethod
15
+ def process(response, return_type: dict, operation_type: str):
16
+ """
17
+ Processes the HTTP response from the API and restores the original data type(s) of the result.
18
+
19
+ This method handles both successful and error responses. For successful responses (HTTP 200),
20
+ it checks the return code inside the response payload and uses the OutputProcessor to convert
21
+ the returned data into its original type(s) based on the `return_type` metadata.
22
+
23
+ Parameters:
24
+ response (requests.Response): The HTTP response object returned by the API call.
25
+ return_type (dict): Metadata describing how to interpret the response data. Keys include:
26
+ - "is_bulk" (bool): Whether the response contains a list of values.
27
+ - "response_type" (type): The expected type of the response values.
28
+ - "type" (type): The container type for the result (e.g., list, tuple).
29
+ - "isENC" (bool): Whether the data is base64-encoded.
30
+ - "charset" (Charset, optional): Character encoding used for decoding bytes.
31
+
32
+ Returns:
33
+ The processed result, either as a single value or a collection (list or tuple),
34
+ along with a tuple of return codes if bulk.
35
+
36
+ Raises:
37
+ Exception: If the return code indicates failure, if the response is malformed,
38
+ or if the data cannot be decoded or restored properly.
39
+ """
40
+ if response.status_code == 200:
41
+ response_json = response.json()
42
+ is_query_success = response_json.get("success")
43
+
44
+ if not is_query_success:
45
+ try:
46
+ message = response.json().get("error_msg", "Unknown Error")
47
+ mapped_message = PythonSDKException.map_error_message(message)
48
+ except Exception:
49
+ mapped_message = "Failed to parse error message from response"
50
+ raise Exception(mapped_message)
51
+
52
+ data = response_json.get("results")
53
+ try:
54
+ result = OutputProcessor.restore_original_type(data, return_type)
55
+
56
+ if not return_type.get("is_bulk", False):
57
+ return result
58
+ else:
59
+ return_type_cls = return_type.get("type", list)
60
+ if return_type_cls == tuple:
61
+ return tuple(result), (return_code[operation_type],) * len(data)
62
+ else:
63
+ return result, (return_code[operation_type],) * len(data)
64
+ except Exception:
65
+ raise Exception(f"26, {log_return_code[26]}") # Error in setting data
66
+ else:
67
+ is_query_success = response.json().get("success", "Unknown Error")
68
+ if not is_query_success:
69
+ try:
70
+ message = response.json().get("error_msg", "Unknown Error")
71
+ mapped_message = PythonSDKException.map_error_message(message)
72
+ except Exception:
73
+ mapped_message = "Failed to parse error message from response"
74
+ raise Exception(mapped_message)
75
+
76
+ message = response.json().get("message", "Could not process the request")
77
+ raise Exception(message)
78
+
@@ -0,0 +1,3 @@
1
+ """Usage statistics collection for migration readiness assessment."""
2
+
3
+ from appython.stats.collector import UsageCollector
@@ -0,0 +1,90 @@
1
+ """
2
+ In-memory usage statistics collector.
3
+
4
+ Accumulates operation counts during a session and flushes to disk on close.
5
+ """
6
+
7
+ import os
8
+ from datetime import date, datetime
9
+
10
+
11
+ def _stats_enabled():
12
+ """Check if stats collection is enabled.
13
+
14
+ Stats are only collected when DEV_EDITION_* env vars are set (i.e. Developer Edition).
15
+ Can be explicitly overridden with PTY_STATS=true/false.
16
+ """
17
+ explicit = os.getenv("PTY_STATS")
18
+ if explicit is not None:
19
+ return explicit.lower() not in ("false", "0", "no", "off")
20
+ # Default: enabled only when Developer Edition env vars are present
21
+ return any(k.startswith("DEV_EDITION_") for k in os.environ)
22
+
23
+
24
+ class UsageCollector:
25
+ """Collects usage statistics in memory during a session.
26
+
27
+ Stats are accumulated per data element and policy user, then flushed
28
+ to disk when flush() is called (typically on session close).
29
+ """
30
+
31
+ def __init__(self, user):
32
+ self._enabled = _stats_enabled()
33
+ self._user = user
34
+ self._data_elements = {}
35
+ self._session_started = date.today().isoformat()
36
+
37
+ @property
38
+ def enabled(self):
39
+ return self._enabled
40
+
41
+ def record_protect(self, data_element):
42
+ """Record a protect operation for a data element."""
43
+ if not self._enabled:
44
+ return
45
+ self._ensure_element(data_element)
46
+ self._data_elements[data_element]["protect_count"] += 1
47
+ self._data_elements[data_element]["last_used"] = date.today().isoformat()
48
+
49
+ def record_unprotect(self, data_element):
50
+ """Record an unprotect operation for a data element."""
51
+ if not self._enabled:
52
+ return
53
+ self._ensure_element(data_element)
54
+ self._data_elements[data_element]["unprotect_count"] += 1
55
+ self._data_elements[data_element]["last_used"] = date.today().isoformat()
56
+
57
+ def record_reprotect(self, source_de, target_de):
58
+ """Record a reprotect operation for source and target data elements."""
59
+ if not self._enabled:
60
+ return
61
+ self._ensure_element(source_de)
62
+ self._ensure_element(target_de)
63
+ self._data_elements[source_de]["reprotect_source_count"] += 1
64
+ self._data_elements[source_de]["last_used"] = date.today().isoformat()
65
+ self._data_elements[target_de]["reprotect_target_count"] += 1
66
+ self._data_elements[target_de]["last_used"] = date.today().isoformat()
67
+
68
+ def get_session_data(self):
69
+ """Return collected stats for this session.
70
+
71
+ Returns:
72
+ dict: Session stats with user, data_elements, and metadata.
73
+ """
74
+ return {
75
+ "user": self._user,
76
+ "data_elements": dict(self._data_elements),
77
+ }
78
+
79
+ def _ensure_element(self, data_element):
80
+ """Ensure a data element entry exists in the collection."""
81
+ if data_element not in self._data_elements:
82
+ today = date.today().isoformat()
83
+ self._data_elements[data_element] = {
84
+ "protect_count": 0,
85
+ "unprotect_count": 0,
86
+ "reprotect_source_count": 0,
87
+ "reprotect_target_count": 0,
88
+ "first_used": today,
89
+ "last_used": today,
90
+ }
@@ -0,0 +1,185 @@
1
+ """
2
+ Persistent storage for usage statistics.
3
+
4
+ Reads, merges, and writes the JSON stats file with file locking
5
+ for multi-process safety.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ import sys
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _SCHEMA_VERSION = "1.0"
18
+
19
+
20
+ # Cross-platform exclusive file locking. fcntl.flock is unix-only; Windows
21
+ # CPython (including Git Bash / MINGW which runs the Windows interpreter)
22
+ # does not ship it. msvcrt.locking provides equivalent semantics there.
23
+ if sys.platform == "win32":
24
+ import msvcrt
25
+
26
+ def _lock_exclusive(fd):
27
+ # msvcrt requires a nonzero byte count. Lock 1 byte from offset 0;
28
+ # this is sufficient as an advisory whole-file lock for our use.
29
+ os.lseek(fd, 0, os.SEEK_SET)
30
+ while True:
31
+ try:
32
+ msvcrt.locking(fd, msvcrt.LK_LOCK, 1)
33
+ return
34
+ except OSError:
35
+ # LK_LOCK blocks ~10s then raises; retry until acquired.
36
+ continue
37
+
38
+ def _unlock(fd):
39
+ try:
40
+ os.lseek(fd, 0, os.SEEK_SET)
41
+ msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
42
+ except OSError:
43
+ pass
44
+ else:
45
+ import fcntl
46
+
47
+ def _lock_exclusive(fd):
48
+ fcntl.flock(fd, fcntl.LOCK_EX)
49
+
50
+ def _unlock(fd):
51
+ fcntl.flock(fd, fcntl.LOCK_UN)
52
+
53
+
54
+ def _stats_path():
55
+ """Return the path to the usage stats file."""
56
+ custom = os.getenv("PTY_STATS_FILE")
57
+ if custom:
58
+ return Path(custom)
59
+ return Path.home() / ".protegrity" / "usage_stats.json"
60
+
61
+
62
+ def _empty_stats():
63
+ """Return a fresh empty stats structure."""
64
+ now = datetime.utcnow().isoformat(timespec="seconds") + "Z"
65
+ return {
66
+ "schema_version": _SCHEMA_VERSION,
67
+ "collected_since": now,
68
+ "last_updated": now,
69
+ "data_elements": {},
70
+ "policy_users": {},
71
+ }
72
+
73
+
74
+ def _read_stats(path):
75
+ """Read existing stats file. Returns None if not found or invalid."""
76
+ if not path.is_file():
77
+ return None
78
+ try:
79
+ with open(path, "r") as f:
80
+ data = json.load(f)
81
+ if data.get("schema_version") == _SCHEMA_VERSION:
82
+ return data
83
+ return None
84
+ except (json.JSONDecodeError, OSError):
85
+ return None
86
+
87
+
88
+ def _merge_session(existing, session_data):
89
+ """Merge a session's collected stats into the existing stats structure.
90
+
91
+ Args:
92
+ existing: The full stats dict (will be mutated).
93
+ session_data: Dict from UsageCollector.get_session_data().
94
+ """
95
+ now = datetime.utcnow().isoformat(timespec="seconds") + "Z"
96
+ existing["last_updated"] = now
97
+
98
+ # Merge data elements
99
+ for de_name, de_stats in session_data.get("data_elements", {}).items():
100
+ if de_name not in existing["data_elements"]:
101
+ existing["data_elements"][de_name] = {
102
+ "protect_count": 0,
103
+ "unprotect_count": 0,
104
+ "reprotect_source_count": 0,
105
+ "reprotect_target_count": 0,
106
+ "first_used": de_stats["first_used"],
107
+ "last_used": de_stats["last_used"],
108
+ }
109
+ target = existing["data_elements"][de_name]
110
+ target["protect_count"] += de_stats["protect_count"]
111
+ target["unprotect_count"] += de_stats["unprotect_count"]
112
+ target["reprotect_source_count"] += de_stats["reprotect_source_count"]
113
+ target["reprotect_target_count"] += de_stats["reprotect_target_count"]
114
+ # Update last_used if session is more recent
115
+ if de_stats["last_used"] > target.get("last_used", ""):
116
+ target["last_used"] = de_stats["last_used"]
117
+ # Keep earliest first_used
118
+ if de_stats["first_used"] < target.get("first_used", "9999-12-31"):
119
+ target["first_used"] = de_stats["first_used"]
120
+
121
+ # Merge policy user
122
+ user = session_data.get("user")
123
+ if user:
124
+ today = datetime.utcnow().strftime("%Y-%m-%d")
125
+ if user not in existing["policy_users"]:
126
+ existing["policy_users"][user] = {
127
+ "session_count": 0,
128
+ "first_used": today,
129
+ "last_used": today,
130
+ }
131
+ existing["policy_users"][user]["session_count"] += 1
132
+ existing["policy_users"][user]["last_used"] = today
133
+
134
+
135
+ def flush_stats(session_data):
136
+ """Flush session stats to disk with file locking.
137
+
138
+ Reads existing stats, merges in session data, writes back atomically.
139
+ Degrades gracefully — never raises exceptions to caller.
140
+
141
+ Args:
142
+ session_data: Dict from UsageCollector.get_session_data().
143
+ """
144
+ try:
145
+ # During interpreter shutdown, modules may be None
146
+ if json is None or os is None:
147
+ return
148
+
149
+ path = _stats_path()
150
+
151
+ # Ensure directory exists
152
+ path.parent.mkdir(parents=True, exist_ok=True)
153
+
154
+ # Open (or create) with exclusive lock
155
+ fd = os.open(str(path), os.O_RDWR | os.O_CREAT, 0o644)
156
+ try:
157
+ _lock_exclusive(fd)
158
+ # Read existing
159
+ with os.fdopen(os.dup(fd), "r") as f:
160
+ try:
161
+ content = f.read()
162
+ existing = json.loads(content) if content.strip() else None
163
+ except (json.JSONDecodeError, OSError):
164
+ existing = None
165
+
166
+ if existing is None or existing.get("schema_version") != _SCHEMA_VERSION:
167
+ existing = _empty_stats()
168
+
169
+ # Merge
170
+ _merge_session(existing, session_data)
171
+
172
+ # Write back (truncate and rewrite)
173
+ os.lseek(fd, 0, os.SEEK_SET)
174
+ os.ftruncate(fd, 0)
175
+ data_bytes = json.dumps(existing, indent=2).encode("utf-8")
176
+ os.write(fd, data_bytes)
177
+ finally:
178
+ _unlock(fd)
179
+ os.close(fd)
180
+
181
+ except Exception as e:
182
+ # Graceful degradation: log warning, never impact SDK operation
183
+ # During interpreter shutdown, modules may already be torn down
184
+ if json is not None and os is not None:
185
+ logger.warning("Failed to write usage stats: %s", e)
@@ -0,0 +1,86 @@
1
+ """
2
+ This module provides helper functions for encoding and decoding data, particularly focusing on
3
+ base64 transformations and character set handling. It includes functions to convert between byte
4
+ strings and base64-encoded strings, as well as functions to decode byte data based on specified
5
+ character sets and operation types.
6
+ """
7
+
8
+ import base64
9
+ from appython.utils.constants import Charset, OP_TYPE as op_type
10
+
11
+
12
+ def convert_b64_bytes_b64_string(data: bytes) -> str:
13
+ """
14
+ Encodes a byte string into a base64-encoded UTF-8 string.
15
+
16
+ Parameters:
17
+ data (bytes): The input byte data to encode.
18
+
19
+ Returns:
20
+ str: A base64-encoded string representation of the input bytes.
21
+ """
22
+ return base64.b64encode(data).decode("utf-8")
23
+
24
+
25
+ def convert_bytes_b64_string(data: bytes, charset: str) -> str:
26
+ """
27
+ Encodes byte data into a base64 string and re-encodes it using the specified character set.
28
+
29
+ Parameters:
30
+ data (bytes): The input byte data to encode.
31
+ charset (str): The character set to use for encoding the base64 string.
32
+
33
+ Returns:
34
+ str: A base64-encoded string re-encoded using the specified charset.
35
+ """
36
+ return base64.b64encode(data).decode("ascii").encode(charset).decode(charset)
37
+
38
+
39
+ def decode_bytes(data: bytes, charset: Charset, is_enc: bool, operation: str) -> str:
40
+ """
41
+ Decodes byte data into a string, optionally applying base64 decoding based on the operation type.
42
+
43
+ Parameters:
44
+ data (bytes): The byte data to decode.
45
+ charset (Charset): The character set to use for decoding.
46
+ is_enc (bool): Indicates whether the data is base64-encoded.
47
+ operation (str): The operation type (e.g., 'PROTECT', 'UNPROTECT', 'REPROTECT').
48
+
49
+ Returns:
50
+ str: The decoded string.
51
+
52
+ Raises:
53
+ Exception: If the operation type is unsupported.
54
+ """
55
+
56
+ if not is_enc:
57
+ return data.decode(charset.name.lower())
58
+ if op_type[operation] in ["UNPROTECT", "REPROTECT"]:
59
+ return convert_b64_bytes_b64_string(data)
60
+ return convert_bytes_b64_string(data, charset.name.lower())
61
+
62
+
63
+ def encode_to_base64_string(data) -> str:
64
+ """
65
+ Converts any input data to a UTF-8 base64-encoded string.
66
+
67
+ Parameters:
68
+ data: The input data to encode (typically str, int, float, etc.).
69
+
70
+ Returns:
71
+ str: A base64-encoded UTF-8 string representation of the input.
72
+ """
73
+ return base64.b64encode(str(data).encode("utf-8")).decode("utf-8")
74
+
75
+
76
+ def get_str_from_b64(data: str):
77
+ """
78
+ Decodes a base64-encoded UTF-8 string back to its original string form.
79
+
80
+ Parameters:
81
+ data (str): The base64-encoded string.
82
+
83
+ Returns:
84
+ str: The decoded original string.
85
+ """
86
+ return base64.b64decode(data.encode("utf-8")).decode("utf-8")