mssql-python 1.0.0__cp314-cp314-win_amd64.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 (73) hide show
  1. mssql_python/__init__.py +307 -0
  2. mssql_python/auth.py +222 -0
  3. mssql_python/connection.py +1493 -0
  4. mssql_python/connection_string_builder.py +113 -0
  5. mssql_python/connection_string_parser.py +381 -0
  6. mssql_python/constants.py +509 -0
  7. mssql_python/cursor.py +2601 -0
  8. mssql_python/db_connection.py +53 -0
  9. mssql_python/ddbc_bindings.cp314-amd64.pyd +0 -0
  10. mssql_python/ddbc_bindings.py +137 -0
  11. mssql_python/exceptions.py +574 -0
  12. mssql_python/helpers.py +357 -0
  13. mssql_python/libs/LICENSING +19 -0
  14. mssql_python/libs/linux/alpine/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  15. mssql_python/libs/linux/alpine/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  16. mssql_python/libs/linux/alpine/arm64/lib/libodbcinst.so.2 +0 -0
  17. mssql_python/libs/linux/alpine/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  18. mssql_python/libs/linux/alpine/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  19. mssql_python/libs/linux/alpine/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  20. mssql_python/libs/linux/alpine/x86_64/lib/libodbcinst.so.2 +0 -0
  21. mssql_python/libs/linux/alpine/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  22. mssql_python/libs/linux/debian_ubuntu/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  23. mssql_python/libs/linux/debian_ubuntu/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  24. mssql_python/libs/linux/debian_ubuntu/arm64/lib/libodbcinst.so.2 +0 -0
  25. mssql_python/libs/linux/debian_ubuntu/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  26. mssql_python/libs/linux/debian_ubuntu/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  27. mssql_python/libs/linux/debian_ubuntu/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  28. mssql_python/libs/linux/debian_ubuntu/x86_64/lib/libodbcinst.so.2 +0 -0
  29. mssql_python/libs/linux/debian_ubuntu/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  30. mssql_python/libs/linux/rhel/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  31. mssql_python/libs/linux/rhel/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  32. mssql_python/libs/linux/rhel/arm64/lib/libodbcinst.so.2 +0 -0
  33. mssql_python/libs/linux/rhel/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  34. mssql_python/libs/linux/rhel/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  35. mssql_python/libs/linux/rhel/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  36. mssql_python/libs/linux/rhel/x86_64/lib/libodbcinst.so.2 +0 -0
  37. mssql_python/libs/linux/rhel/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  38. mssql_python/libs/linux/suse/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  39. mssql_python/libs/linux/suse/x86_64/lib/libodbcinst.so.2 +0 -0
  40. mssql_python/libs/linux/suse/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  41. mssql_python/libs/macos/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  42. mssql_python/libs/macos/arm64/lib/libltdl.7.dylib +0 -0
  43. mssql_python/libs/macos/arm64/lib/libmsodbcsql.18.dylib +0 -0
  44. mssql_python/libs/macos/arm64/lib/libodbc.2.dylib +0 -0
  45. mssql_python/libs/macos/arm64/lib/libodbcinst.2.dylib +0 -0
  46. mssql_python/libs/macos/arm64/share/doc/msodbcsql18/LICENSE.txt +101 -0
  47. mssql_python/libs/macos/arm64/share/doc/msodbcsql18/RELEASE_NOTES +465 -0
  48. mssql_python/libs/macos/arm64/share/msodbcsql18/resources/en_US/msodbcsqlr18.rll +0 -0
  49. mssql_python/libs/macos/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  50. mssql_python/libs/macos/x86_64/lib/libltdl.7.dylib +0 -0
  51. mssql_python/libs/macos/x86_64/lib/libmsodbcsql.18.dylib +0 -0
  52. mssql_python/libs/macos/x86_64/lib/libodbc.2.dylib +0 -0
  53. mssql_python/libs/macos/x86_64/lib/libodbcinst.2.dylib +0 -0
  54. mssql_python/libs/macos/x86_64/share/doc/msodbcsql18/LICENSE.txt +101 -0
  55. mssql_python/libs/macos/x86_64/share/doc/msodbcsql18/RELEASE_NOTES +465 -0
  56. mssql_python/libs/macos/x86_64/share/msodbcsql18/resources/en_US/msodbcsqlr18.rll +0 -0
  57. mssql_python/libs/windows/x64/1033/msodbcsqlr18.rll +0 -0
  58. mssql_python/libs/windows/x64/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  59. mssql_python/libs/windows/x64/MICROSOFT_VISUAL_STUDIO_LICENSE.txt +54 -0
  60. mssql_python/libs/windows/x64/msodbcdiag18.dll +0 -0
  61. mssql_python/libs/windows/x64/msodbcsql18.dll +0 -0
  62. mssql_python/libs/windows/x64/mssql-auth.dll +0 -0
  63. mssql_python/logging.py +610 -0
  64. mssql_python/mssql_python.pyi +375 -0
  65. mssql_python/msvcp140.dll +0 -0
  66. mssql_python/pooling.py +125 -0
  67. mssql_python/row.py +194 -0
  68. mssql_python/type.py +143 -0
  69. mssql_python-1.0.0.dist-info/METADATA +76 -0
  70. mssql_python-1.0.0.dist-info/RECORD +73 -0
  71. mssql_python-1.0.0.dist-info/WHEEL +5 -0
  72. mssql_python-1.0.0.dist-info/licenses/LICENSE +47 -0
  73. mssql_python-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,113 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation.
3
+ Licensed under the MIT license.
4
+
5
+ Connection string builder for mssql-python.
6
+
7
+ Reconstructs ODBC connection strings from parameter dictionaries
8
+ with proper escaping and formatting per MS-ODBCSTR specification.
9
+ """
10
+
11
+ from typing import Dict, Optional
12
+ from mssql_python.constants import _CONNECTION_STRING_DRIVER_KEY
13
+
14
+ class _ConnectionStringBuilder:
15
+ """
16
+ Internal builder for ODBC connection strings. Not part of public API.
17
+
18
+ Handles proper escaping of special characters and reconstructs
19
+ connection strings in ODBC format.
20
+ """
21
+
22
+ def __init__(self, initial_params: Optional[Dict[str, str]] = None):
23
+ """
24
+ Initialize the builder with optional initial parameters.
25
+
26
+ Args:
27
+ initial_params: Dictionary of initial connection parameters
28
+ """
29
+ self._params: Dict[str, str] = initial_params.copy() if initial_params else {}
30
+
31
+ def add_param(self, key: str, value: str) -> '_ConnectionStringBuilder':
32
+ """
33
+ Add or update a connection parameter.
34
+
35
+ Args:
36
+ key: Parameter name (should be normalized canonical name)
37
+ value: Parameter value
38
+
39
+ Returns:
40
+ Self for method chaining
41
+ """
42
+ self._params[key] = str(value)
43
+ return self
44
+
45
+ def build(self) -> str:
46
+ """
47
+ Build the final connection string.
48
+
49
+ Returns:
50
+ ODBC-formatted connection string with proper escaping
51
+
52
+ Note:
53
+ - Driver parameter is placed first
54
+ - Other parameters are sorted for consistency
55
+ - Values are escaped if they contain special characters
56
+ """
57
+ parts = []
58
+
59
+ # Build in specific order: Driver first, then others
60
+ if _CONNECTION_STRING_DRIVER_KEY in self._params:
61
+ parts.append(f"Driver={self._escape_value(self._params['Driver'])}")
62
+
63
+ # Add other parameters (sorted for consistency)
64
+ for key in sorted(self._params.keys()):
65
+ if key == 'Driver':
66
+ continue # Already added
67
+
68
+ value = self._params[key]
69
+ escaped_value = self._escape_value(value)
70
+ parts.append(f"{key}={escaped_value}")
71
+
72
+ # Join with semicolons
73
+ return ';'.join(parts)
74
+
75
+ def _escape_value(self, value: str) -> str:
76
+ """
77
+ Escape a parameter value if it contains special characters.
78
+
79
+ Per MS-ODBCSTR specification:
80
+ - Values containing ';', '{', '}', '=', or spaces should be braced for safety
81
+ - '}' inside braced values is escaped as '}}'
82
+ - '{' inside braced values is escaped as '{{'
83
+
84
+ Args:
85
+ value: Parameter value to escape
86
+
87
+ Returns:
88
+ Escaped value (possibly wrapped in braces)
89
+
90
+ Examples:
91
+ >>> builder = _ConnectionStringBuilder()
92
+ >>> builder._escape_value("localhost")
93
+ 'localhost'
94
+ >>> builder._escape_value("local;host")
95
+ '{local;host}'
96
+ >>> builder._escape_value("p}w{d")
97
+ '{p}}w{{d}'
98
+ >>> builder._escape_value("ODBC Driver 18 for SQL Server")
99
+ '{ODBC Driver 18 for SQL Server}'
100
+ """
101
+ if not value:
102
+ return value
103
+
104
+ # Check if value contains special characters that require bracing
105
+ # Include spaces and = for safety, even though technically not always required
106
+ needs_braces = any(ch in value for ch in ';{}= ')
107
+
108
+ if needs_braces:
109
+ # Escape existing braces by doubling them
110
+ escaped = value.replace('}', '}}').replace('{', '{{')
111
+ return f'{{{escaped}}}'
112
+ else:
113
+ return value
@@ -0,0 +1,381 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation.
3
+ Licensed under the MIT license.
4
+
5
+ ODBC connection string parser for mssql-python.
6
+
7
+ Handles ODBC-specific syntax per MS-ODBCSTR specification:
8
+ - Semicolon-separated key=value pairs
9
+ - Braced values: {value}
10
+ - Escaped braces: }} → }, {{ → {
11
+
12
+ Parser behavior:
13
+ - Validates all key=value pairs
14
+ - Raises exceptions for malformed syntax (missing values, unknown keywords, duplicates)
15
+ - Collects all errors and reports them together
16
+ """
17
+
18
+ from typing import Dict, Tuple, Optional
19
+ from mssql_python.exceptions import ConnectionStringParseError
20
+ from mssql_python.constants import _ALLOWED_CONNECTION_STRING_PARAMS, _RESERVED_PARAMETERS
21
+ from mssql_python.helpers import sanitize_user_input
22
+ from mssql_python.logging import logger
23
+
24
+
25
+ class _ConnectionStringParser:
26
+ """
27
+ Internal parser for ODBC connection strings. Not part of public API.
28
+
29
+ Implements the ODBC Connection String format as specified in MS-ODBCSTR.
30
+ Handles braced values, escaped characters, and proper tokenization.
31
+
32
+ Validates connection strings and raises errors for:
33
+ - Unknown/unrecognized keywords
34
+ - Duplicate keywords
35
+ - Incomplete specifications (keyword with no value)
36
+
37
+ Reference: https://learn.microsoft.com/en-us/openspecs/sql_server_protocols/ms-odbcstr/55953f0e-2d30-4ad4-8e56-b4207e491409
38
+ """
39
+
40
+ def __init__(self, validate_keywords: bool = False) -> None:
41
+ """
42
+ Initialize the parser.
43
+
44
+ Args:
45
+ validate_keywords: Whether to validate keywords against the allow-list.
46
+ If False, pure parsing without validation is performed.
47
+ This is useful for testing parsing logic independently
48
+ or when validation is handled separately.
49
+ """
50
+ self._validate_keywords = validate_keywords
51
+
52
+ @classmethod
53
+ def normalize_key(cls, key: str) -> Optional[str]:
54
+ """
55
+ Normalize a parameter key to its canonical form.
56
+
57
+ Args:
58
+ key: Parameter key from connection string (case-insensitive)
59
+
60
+ Returns:
61
+ Canonical parameter name if allowed, None otherwise
62
+
63
+ Examples:
64
+ >>> _ConnectionStringParser.normalize_key('SERVER')
65
+ 'Server'
66
+ >>> _ConnectionStringParser.normalize_key('uid')
67
+ 'UID'
68
+ >>> _ConnectionStringParser.normalize_key('UnsupportedParam')
69
+ None
70
+ """
71
+ key_lower = key.lower().strip()
72
+ return _ALLOWED_CONNECTION_STRING_PARAMS.get(key_lower)
73
+
74
+ @staticmethod
75
+ def _normalize_params(params: Dict[str, str], warn_rejected: bool = True) -> Dict[str, str]:
76
+ """
77
+ Normalize and filter parameters against the allow-list (internal use only).
78
+
79
+ This method performs several operations:
80
+ - Normalizes parameter names (e.g., addr/address → Server, uid → UID)
81
+ - Filters out parameters not in the allow-list
82
+ - Removes reserved parameters (Driver, APP)
83
+ - Deduplicates via normalized keys
84
+
85
+ Args:
86
+ params: Dictionary of connection string parameters (keys should be lowercase)
87
+ warn_rejected: Whether to log warnings for rejected parameters
88
+
89
+ Returns:
90
+ Dictionary containing only allowed parameters with normalized keys
91
+
92
+ Note:
93
+ Driver and APP parameters are filtered here but will be set by
94
+ the driver in _construct_connection_string to maintain control.
95
+ """
96
+ filtered = {}
97
+
98
+ # The rejected list should ideally be empty when used in the normal connection
99
+ # flow, since the parser validates against the allowlist first and raises
100
+ # errors for unknown parameters. This filtering is primarily a safety net.
101
+ rejected = []
102
+
103
+ for key, value in params.items():
104
+ normalized_key = _ConnectionStringParser.normalize_key(key)
105
+
106
+ if normalized_key:
107
+ # Skip Driver and APP - these are controlled by the driver
108
+ if normalized_key in _RESERVED_PARAMETERS:
109
+ continue
110
+
111
+ # Parameter is allowed
112
+ filtered[normalized_key] = value
113
+ else:
114
+ # Parameter is not in allow-list
115
+ # Note: In normal flow, this should be empty since parser validates first
116
+ rejected.append(key)
117
+
118
+ # Log all rejected parameters together if any were found
119
+ if rejected and warn_rejected:
120
+ safe_keys = [sanitize_user_input(key) for key in rejected]
121
+ logger.debug(
122
+ f"Connection string parameters not in allow-list and will be ignored: {', '.join(safe_keys)}"
123
+ )
124
+
125
+ return filtered
126
+
127
+ def _parse(self, connection_str: str) -> Dict[str, str]:
128
+ """
129
+ Parse a connection string into a dictionary of parameters.
130
+
131
+ Validates the connection string and raises ConnectionStringParseError
132
+ if any issues are found (unknown keywords, duplicates, missing values).
133
+
134
+ Args:
135
+ connection_str: ODBC-format connection string
136
+
137
+ Returns:
138
+ Dictionary mapping parameter names (lowercase) to values
139
+
140
+ Raises:
141
+ ConnectionStringParseError: If validation errors are found
142
+
143
+ Examples:
144
+ >>> parser = _ConnectionStringParser()
145
+ >>> result = parser._parse("Server=localhost;Database=mydb")
146
+ {'server': 'localhost', 'database': 'mydb'}
147
+
148
+ >>> parser._parse("Server={;local;};PWD={p}}w{{d}")
149
+ {'server': ';local;', 'pwd': 'p}w{d'}
150
+
151
+ >>> parser._parse("Server=localhost;Server=other")
152
+ ConnectionStringParseError: Duplicate keyword 'server'
153
+ """
154
+ if not connection_str:
155
+ return {}
156
+
157
+ connection_str = connection_str.strip()
158
+ if not connection_str:
159
+ return {}
160
+
161
+ # Collect all errors for batch reporting
162
+ errors = []
163
+
164
+ # Dictionary to store parsed key=value pairs
165
+ params = {}
166
+
167
+ # Track which keys we've seen to detect duplicates
168
+ seen_keys = {} # Maps normalized key -> first occurrence position
169
+
170
+ # Track current position in the string
171
+ current_pos = 0
172
+ str_len = len(connection_str)
173
+
174
+ # Main parsing loop
175
+ while current_pos < str_len:
176
+ # Skip leading whitespace and semicolons
177
+ while current_pos < str_len and connection_str[current_pos] in ' \t;':
178
+ current_pos += 1
179
+
180
+ if current_pos >= str_len:
181
+ break
182
+
183
+ # Parse the key
184
+ key_start = current_pos
185
+
186
+ # Advance until we hit '=', ';', or end of string
187
+ while current_pos < str_len and connection_str[current_pos] not in '=;':
188
+ current_pos += 1
189
+
190
+ # Check if we found a valid '=' separator
191
+ if current_pos >= str_len or connection_str[current_pos] != '=':
192
+ # ERROR: No '=' found - incomplete specification
193
+ incomplete_text = connection_str[key_start:current_pos].strip()
194
+ if incomplete_text:
195
+ errors.append(f"Incomplete specification: keyword '{incomplete_text}' has no value (missing '=')")
196
+ # Skip to next semicolon
197
+ while current_pos < str_len and connection_str[current_pos] != ';':
198
+ current_pos += 1
199
+ continue
200
+
201
+ # Extract and normalize the key
202
+ key = connection_str[key_start:current_pos].strip().lower()
203
+
204
+ # ERROR: Empty key
205
+ if not key:
206
+ errors.append("Empty keyword found (format: =value)")
207
+ current_pos += 1 # Skip the '='
208
+ # Skip to next semicolon
209
+ while current_pos < str_len and connection_str[current_pos] != ';':
210
+ current_pos += 1
211
+ continue
212
+
213
+ # Move past the '='
214
+ current_pos += 1
215
+
216
+ # Parse the value
217
+ try:
218
+ value, current_pos = self._parse_value(connection_str, current_pos)
219
+
220
+ # ERROR: Empty value
221
+ if not value:
222
+ errors.append(f"Empty value for keyword '{key}' (all connection string parameters must have non-empty values)")
223
+
224
+ # Check for duplicates
225
+ if key in seen_keys:
226
+ errors.append(f"Duplicate keyword '{key}' found")
227
+ else:
228
+ seen_keys[key] = True
229
+ params[key] = value
230
+
231
+ except ValueError as e:
232
+ errors.append(f"Error parsing value for keyword '{key}': {e}")
233
+ # Skip to next semicolon
234
+ while current_pos < str_len and connection_str[current_pos] != ';':
235
+ current_pos += 1
236
+
237
+ # Validate keywords against allowlist if validation is enabled
238
+ if self._validate_keywords:
239
+ unknown_keys = []
240
+ reserved_keys = []
241
+
242
+ for key in params.keys():
243
+ # Check if this key can be normalized (i.e., it's known)
244
+ normalized_key = _ConnectionStringParser.normalize_key(key)
245
+
246
+ if normalized_key is None:
247
+ # Unknown keyword
248
+ unknown_keys.append(key)
249
+ elif normalized_key in _RESERVED_PARAMETERS:
250
+ # Reserved keyword - user cannot set these
251
+ reserved_keys.append(key)
252
+
253
+ if reserved_keys:
254
+ for key in reserved_keys:
255
+ errors.append(
256
+ f"Reserved keyword '{key}' is controlled by the driver and cannot be specified by the user"
257
+ )
258
+
259
+ if unknown_keys:
260
+ for key in unknown_keys:
261
+ errors.append(f"Unknown keyword '{key}' is not recognized")
262
+
263
+ # If we collected any errors, raise them all together
264
+ if errors:
265
+ raise ConnectionStringParseError(errors)
266
+
267
+ return params
268
+
269
+ def _parse_value(self, connection_str: str, start_pos: int) -> Tuple[str, int]:
270
+ """
271
+ Parse a parameter value from the connection string.
272
+
273
+ Handles both simple values and braced values with escaping.
274
+
275
+ Args:
276
+ connection_str: The connection string
277
+ start_pos: Starting position of the value
278
+
279
+ Returns:
280
+ Tuple of (parsed_value, new_position)
281
+
282
+ Raises:
283
+ ValueError: If braced value is not properly closed
284
+ """
285
+ str_len = len(connection_str)
286
+
287
+ # Skip leading whitespace before the value
288
+ while start_pos < str_len and connection_str[start_pos] in ' \t':
289
+ start_pos += 1
290
+
291
+ # If we've consumed the entire string or reached a semicolon, return empty value
292
+ if start_pos >= str_len:
293
+ return '', start_pos
294
+
295
+ # Determine if this is a braced value or simple value
296
+ if connection_str[start_pos] == '{':
297
+ return self._parse_braced_value(connection_str, start_pos)
298
+ else:
299
+ return self._parse_simple_value(connection_str, start_pos)
300
+
301
+ def _parse_simple_value(self, connection_str: str, start_pos: int) -> Tuple[str, int]:
302
+ """
303
+ Parse a simple (non-braced) value up to the next semicolon.
304
+
305
+ Args:
306
+ connection_str: The connection string
307
+ start_pos: Starting position of the value
308
+
309
+ Returns:
310
+ Tuple of (parsed_value, new_position)
311
+ """
312
+ str_len = len(connection_str)
313
+ value_start = start_pos
314
+
315
+ # Read characters until we hit a semicolon or end of string
316
+ while start_pos < str_len and connection_str[start_pos] != ';':
317
+ start_pos += 1
318
+
319
+ # Extract the value and strip trailing whitespace
320
+ value = connection_str[value_start:start_pos].rstrip()
321
+ return value, start_pos
322
+
323
+ def _parse_braced_value(self, connection_str: str, start_pos: int) -> Tuple[str, int]:
324
+ """
325
+ Parse a braced value with proper handling of escaped braces.
326
+
327
+ Braced values:
328
+ - Start with '{' and end with '}'
329
+ - '}' inside the value is escaped as '}}'
330
+ - '{' inside the value is escaped as '{{'
331
+ - Can contain semicolons and other special characters
332
+
333
+ Args:
334
+ connection_str: The connection string
335
+ start_pos: Starting position (should point to opening '{')
336
+
337
+ Returns:
338
+ Tuple of (parsed_value, new_position)
339
+
340
+ Raises:
341
+ ValueError: If the braced value is not closed (missing '}')
342
+ """
343
+ str_len = len(connection_str)
344
+ brace_start_pos = start_pos
345
+
346
+ # Skip the opening '{'
347
+ start_pos += 1
348
+
349
+ # Build the value character by character
350
+ value = []
351
+
352
+ while start_pos < str_len:
353
+ ch = connection_str[start_pos]
354
+
355
+ if ch == '}':
356
+ # Check if next character is also '}' (escaped brace)
357
+ if start_pos + 1 < str_len and connection_str[start_pos + 1] == '}':
358
+ # Escaped right brace: '}}' → '}'
359
+ value.append('}')
360
+ start_pos += 2
361
+ else:
362
+ # Single '}' means end of braced value
363
+ start_pos += 1
364
+ return ''.join(value), start_pos
365
+ elif ch == '{':
366
+ # Check if it's an escaped left brace
367
+ if start_pos + 1 < str_len and connection_str[start_pos + 1] == '{':
368
+ # Escaped left brace: '{{' → '{'
369
+ value.append('{')
370
+ start_pos += 2
371
+ else:
372
+ # Single '{' inside braced value - keep it as is
373
+ value.append(ch)
374
+ start_pos += 1
375
+ else:
376
+ # Regular character
377
+ value.append(ch)
378
+ start_pos += 1
379
+
380
+ # Reached end without finding closing '}'
381
+ raise ValueError(f"Unclosed braced value starting at position {brace_start_pos}")