gopher-mcp-python 0.1.1__tar.gz → 0.1.2__tar.gz

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 (27) hide show
  1. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/PKG-INFO +1 -1
  2. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/__init__.py +27 -1
  3. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/ffi/__init__.py +8 -1
  4. gopher_mcp_python-0.1.2/gopher_mcp_python/ffi/auth/__init__.py +82 -0
  5. gopher_mcp_python-0.1.2/gopher_mcp_python/ffi/auth/auth_client.py +547 -0
  6. gopher_mcp_python-0.1.2/gopher_mcp_python/ffi/auth/loader.py +404 -0
  7. gopher_mcp_python-0.1.2/gopher_mcp_python/ffi/auth/types.py +135 -0
  8. gopher_mcp_python-0.1.2/gopher_mcp_python/ffi/auth/validation_options.py +239 -0
  9. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python.egg-info/PKG-INFO +1 -1
  10. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python.egg-info/SOURCES.txt +5 -0
  11. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/pyproject.toml +1 -1
  12. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/LICENSE +0 -0
  13. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/README.md +0 -0
  14. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/agent.py +0 -0
  15. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/config.py +0 -0
  16. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/errors.py +0 -0
  17. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/ffi/library.py +0 -0
  18. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/result.py +0 -0
  19. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/server_config.py +0 -0
  20. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python.egg-info/dependency_links.txt +0 -0
  21. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python.egg-info/requires.txt +0 -0
  22. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python.egg-info/top_level.txt +0 -0
  23. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/setup.cfg +0 -0
  24. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/setup.py +0 -0
  25. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/tests/test_config.py +0 -0
  26. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/tests/test_ffi.py +0 -0
  27. {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/tests/test_result.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gopher-mcp-python
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Python SDK for Gopher MCP - AI Agent orchestration framework with native performance
5
5
  Author-email: Gopher Security <dev@gophersecurity.com>
6
6
  License: Apache-2.0
@@ -29,7 +29,23 @@ from gopher_mcp_python.errors import AgentError, ApiKeyError, ConnectionError, T
29
29
  from gopher_mcp_python.server_config import ServerConfig
30
30
  from gopher_mcp_python.ffi import GopherOrchLibrary
31
31
 
32
- __version__ = "0.1.1"
32
+ # Auth module re-exports
33
+ from gopher_mcp_python.ffi.auth import (
34
+ # Types
35
+ GopherAuthError,
36
+ ValidationResult,
37
+ TokenPayload,
38
+ GopherAuthContext,
39
+ # Classes
40
+ GopherAuthClient,
41
+ GopherValidationOptions,
42
+ # Functions
43
+ gopher_init_auth_library,
44
+ gopher_shutdown_auth_library,
45
+ is_auth_available,
46
+ )
47
+
48
+ __version__ = "0.1.2"
33
49
 
34
50
  __all__ = [
35
51
  # Main classes
@@ -47,6 +63,16 @@ __all__ = [
47
63
  "TimeoutError",
48
64
  # FFI
49
65
  "GopherOrchLibrary",
66
+ # Auth
67
+ "GopherAuthError",
68
+ "ValidationResult",
69
+ "TokenPayload",
70
+ "GopherAuthContext",
71
+ "GopherAuthClient",
72
+ "GopherValidationOptions",
73
+ "gopher_init_auth_library",
74
+ "gopher_shutdown_auth_library",
75
+ "is_auth_available",
50
76
  # Version
51
77
  "__version__",
52
78
  ]
@@ -4,4 +4,11 @@ FFI bindings for the gopher-mcp-python native library.
4
4
 
5
5
  from gopher_mcp_python.ffi.library import GopherOrchLibrary, GopherOrchHandle
6
6
 
7
- __all__ = ["GopherOrchLibrary", "GopherOrchHandle"]
7
+ # Auth module
8
+ from gopher_mcp_python.ffi import auth
9
+
10
+ __all__ = [
11
+ "GopherOrchLibrary",
12
+ "GopherOrchHandle",
13
+ "auth",
14
+ ]
@@ -0,0 +1,82 @@
1
+ """
2
+ Auth FFI bindings for gopher-auth native library.
3
+
4
+ Provides Python bindings for JWT token validation and OAuth support
5
+ via the gopher-orch native library.
6
+ """
7
+
8
+ from gopher_mcp_python.ffi.auth.types import (
9
+ GopherAuthError,
10
+ ERROR_DESCRIPTIONS,
11
+ ValidationResult,
12
+ TokenPayload,
13
+ GopherAuthContext,
14
+ is_gopher_auth_error,
15
+ get_error_description,
16
+ gopher_create_empty_auth_context,
17
+ )
18
+
19
+ from gopher_mcp_python.ffi.auth.loader import (
20
+ GopherAuthClientPtr,
21
+ GopherAuthPayloadPtr,
22
+ GopherAuthOptionsPtr,
23
+ GopherAuthValidationResult,
24
+ load_library,
25
+ is_library_loaded,
26
+ get_library,
27
+ is_auth_available,
28
+ get_auth_functions,
29
+ )
30
+
31
+ from gopher_mcp_python.ffi.auth.validation_options import (
32
+ GopherValidationOptions,
33
+ gopher_create_validation_options,
34
+ )
35
+
36
+ from gopher_mcp_python.ffi.auth.auth_client import (
37
+ GopherAuthClient,
38
+ gopher_init_auth_library,
39
+ gopher_shutdown_auth_library,
40
+ gopher_get_auth_library_version,
41
+ gopher_is_auth_library_initialized,
42
+ gopher_generate_www_authenticate_header,
43
+ gopher_generate_www_authenticate_header_v2,
44
+ )
45
+
46
+ __all__ = [
47
+ # Enums
48
+ "GopherAuthError",
49
+ # Constants
50
+ "ERROR_DESCRIPTIONS",
51
+ # Dataclasses
52
+ "ValidationResult",
53
+ "TokenPayload",
54
+ "GopherAuthContext",
55
+ # Type functions
56
+ "is_gopher_auth_error",
57
+ "get_error_description",
58
+ "gopher_create_empty_auth_context",
59
+ # Pointer types
60
+ "GopherAuthClientPtr",
61
+ "GopherAuthPayloadPtr",
62
+ "GopherAuthOptionsPtr",
63
+ # Structures
64
+ "GopherAuthValidationResult",
65
+ # Loader functions
66
+ "load_library",
67
+ "is_library_loaded",
68
+ "get_library",
69
+ "is_auth_available",
70
+ "get_auth_functions",
71
+ # Validation options
72
+ "GopherValidationOptions",
73
+ "gopher_create_validation_options",
74
+ # Auth client
75
+ "GopherAuthClient",
76
+ "gopher_init_auth_library",
77
+ "gopher_shutdown_auth_library",
78
+ "gopher_get_auth_library_version",
79
+ "gopher_is_auth_library_initialized",
80
+ "gopher_generate_www_authenticate_header",
81
+ "gopher_generate_www_authenticate_header_v2",
82
+ ]
@@ -0,0 +1,547 @@
1
+ """
2
+ AuthClient - JWT token validation client.
3
+
4
+ Provides high-level API for validating JWT tokens and extracting
5
+ payload information using the gopher-auth native library.
6
+ """
7
+
8
+ from ctypes import byref, c_char_p, c_int64, c_void_p
9
+ from typing import List, Optional, Tuple
10
+
11
+ from gopher_mcp_python.ffi.auth.loader import (
12
+ load_library,
13
+ get_auth_functions,
14
+ is_auth_available,
15
+ GopherAuthValidationResult,
16
+ )
17
+ from gopher_mcp_python.ffi.auth.types import (
18
+ GopherAuthError,
19
+ ValidationResult,
20
+ TokenPayload,
21
+ get_error_description,
22
+ )
23
+ from gopher_mcp_python.ffi.auth.validation_options import GopherValidationOptions
24
+
25
+
26
+ # ============================================================================
27
+ # Library Lifecycle Functions
28
+ # ============================================================================
29
+
30
+ _auth_initialized: bool = False
31
+
32
+
33
+ def gopher_init_auth_library() -> bool:
34
+ """
35
+ Initialize the auth library.
36
+
37
+ This must be called before using any auth functions.
38
+
39
+ Returns:
40
+ True if initialization successful, False otherwise.
41
+ """
42
+ global _auth_initialized
43
+
44
+ if _auth_initialized:
45
+ return True
46
+
47
+ if not load_library():
48
+ return False
49
+
50
+ if not is_auth_available():
51
+ return False
52
+
53
+ funcs = get_auth_functions()
54
+ auth_init = funcs.get("auth_init")
55
+ if auth_init is None:
56
+ return False
57
+
58
+ result = auth_init()
59
+ if result == GopherAuthError.SUCCESS:
60
+ _auth_initialized = True
61
+ return True
62
+
63
+ return False
64
+
65
+
66
+ def gopher_shutdown_auth_library() -> bool:
67
+ """
68
+ Shutdown the auth library and release resources.
69
+
70
+ Returns:
71
+ True if shutdown successful, False otherwise.
72
+ """
73
+ global _auth_initialized
74
+
75
+ if not _auth_initialized:
76
+ return True
77
+
78
+ funcs = get_auth_functions()
79
+ auth_shutdown = funcs.get("auth_shutdown")
80
+ if auth_shutdown is None:
81
+ return False
82
+
83
+ result = auth_shutdown()
84
+ if result == GopherAuthError.SUCCESS:
85
+ _auth_initialized = False
86
+ return True
87
+
88
+ return False
89
+
90
+
91
+ def gopher_get_auth_library_version() -> Optional[str]:
92
+ """
93
+ Get the auth library version string.
94
+
95
+ Returns:
96
+ Version string, or None if unavailable.
97
+ """
98
+ if not load_library():
99
+ return None
100
+
101
+ funcs = get_auth_functions()
102
+ auth_version = funcs.get("auth_version")
103
+ if auth_version is None:
104
+ return None
105
+
106
+ result = auth_version()
107
+ if result:
108
+ return result.decode("utf-8")
109
+ return None
110
+
111
+
112
+ def gopher_is_auth_library_initialized() -> bool:
113
+ """
114
+ Check if the auth library has been initialized.
115
+
116
+ Returns:
117
+ True if initialized, False otherwise.
118
+ """
119
+ return _auth_initialized
120
+
121
+
122
+ # ============================================================================
123
+ # WWW-Authenticate Header Generation
124
+ # ============================================================================
125
+
126
+
127
+ def gopher_generate_www_authenticate_header(
128
+ realm: str,
129
+ error: str = "",
130
+ description: str = "",
131
+ ) -> Optional[str]:
132
+ """
133
+ Generate a WWW-Authenticate header for 401 responses.
134
+
135
+ Args:
136
+ realm: The authentication realm.
137
+ error: Optional error code (e.g., "invalid_token").
138
+ description: Optional error description.
139
+
140
+ Returns:
141
+ The WWW-Authenticate header value, or None on error.
142
+ """
143
+ if not load_library():
144
+ return None
145
+
146
+ funcs = get_auth_functions()
147
+ generate_func = funcs.get("generate_www_authenticate")
148
+ if generate_func is None:
149
+ return None
150
+
151
+ header_out = c_char_p()
152
+ result = generate_func(
153
+ realm.encode("utf-8"),
154
+ error.encode("utf-8"),
155
+ description.encode("utf-8"),
156
+ byref(header_out),
157
+ )
158
+
159
+ if result != GopherAuthError.SUCCESS:
160
+ return None
161
+
162
+ if header_out.value:
163
+ header = header_out.value.decode("utf-8")
164
+ # Free the allocated string
165
+ free_string = funcs.get("free_string")
166
+ if free_string:
167
+ free_string(header_out)
168
+ return header
169
+
170
+ return None
171
+
172
+
173
+ def gopher_generate_www_authenticate_header_v2(
174
+ realm: str,
175
+ resource_metadata_url: str,
176
+ scopes: List[str],
177
+ error: str = "",
178
+ error_description: str = "",
179
+ ) -> Optional[str]:
180
+ """
181
+ Generate a WWW-Authenticate header with RFC 9728 support.
182
+
183
+ Args:
184
+ realm: The authentication realm.
185
+ resource_metadata_url: URL to the OAuth protected resource metadata.
186
+ scopes: List of required scopes.
187
+ error: Optional error code.
188
+ error_description: Optional error description.
189
+
190
+ Returns:
191
+ The WWW-Authenticate header value, or None on error.
192
+ """
193
+ if not load_library():
194
+ return None
195
+
196
+ funcs = get_auth_functions()
197
+ generate_func = funcs.get("generate_www_authenticate_v2")
198
+ if generate_func is None:
199
+ return None
200
+
201
+ scopes_str = " ".join(scopes)
202
+ header_out = c_char_p()
203
+ result = generate_func(
204
+ realm.encode("utf-8"),
205
+ resource_metadata_url.encode("utf-8"),
206
+ scopes_str.encode("utf-8"),
207
+ error.encode("utf-8"),
208
+ error_description.encode("utf-8"),
209
+ byref(header_out),
210
+ )
211
+
212
+ if result != GopherAuthError.SUCCESS:
213
+ return None
214
+
215
+ if header_out.value:
216
+ header = header_out.value.decode("utf-8")
217
+ # Free the allocated string
218
+ free_string = funcs.get("free_string")
219
+ if free_string:
220
+ free_string(header_out)
221
+ return header
222
+
223
+ return None
224
+
225
+
226
+ # ============================================================================
227
+ # AuthClient Class
228
+ # ============================================================================
229
+
230
+
231
+ class GopherAuthClient:
232
+ """
233
+ JWT token validation client.
234
+
235
+ Provides methods for validating JWT tokens and extracting payload
236
+ information using JWKS-based signature verification.
237
+
238
+ Usage:
239
+ # Using context manager (recommended)
240
+ with GopherAuthClient("https://auth.example.com/.well-known/jwks.json",
241
+ "https://auth.example.com") as client:
242
+ result = client.validate_token(token)
243
+ if result.valid:
244
+ payload = client.extract_payload(token)
245
+
246
+ # Manual lifecycle management
247
+ client = GopherAuthClient(jwks_uri, issuer)
248
+ try:
249
+ result, payload = client.validate_and_extract(token)
250
+ finally:
251
+ client.destroy()
252
+ """
253
+
254
+ def __init__(self, jwks_uri: str, issuer: str) -> None:
255
+ """
256
+ Create a new AuthClient instance.
257
+
258
+ Args:
259
+ jwks_uri: URL to the JWKS endpoint.
260
+ issuer: Expected token issuer.
261
+
262
+ Raises:
263
+ RuntimeError: If the library is not loaded or client creation fails.
264
+ """
265
+ self._handle: Optional[c_void_p] = None
266
+ self._destroyed: bool = False
267
+
268
+ if not load_library():
269
+ raise RuntimeError("Failed to load gopher-orch library")
270
+
271
+ if not is_auth_available():
272
+ raise RuntimeError("Auth functions not available in library")
273
+
274
+ # Initialize library if needed
275
+ if not gopher_is_auth_library_initialized():
276
+ if not gopher_init_auth_library():
277
+ raise RuntimeError("Failed to initialize auth library")
278
+
279
+ funcs = get_auth_functions()
280
+ client_create = funcs.get("client_create")
281
+ if client_create is None:
282
+ raise RuntimeError("client_create function not available")
283
+
284
+ handle = c_void_p()
285
+ result = client_create(
286
+ byref(handle),
287
+ jwks_uri.encode("utf-8"),
288
+ issuer.encode("utf-8"),
289
+ )
290
+
291
+ if result != GopherAuthError.SUCCESS:
292
+ raise RuntimeError(
293
+ f"Failed to create auth client: {get_error_description(result)}"
294
+ )
295
+
296
+ self._handle = handle
297
+
298
+ def set_option(self, key: str, value: str) -> bool:
299
+ """
300
+ Set a client option.
301
+
302
+ Args:
303
+ key: Option name (e.g., "cache_duration", "auto_refresh", "request_timeout").
304
+ value: Option value as string.
305
+
306
+ Returns:
307
+ True if option was set successfully, False otherwise.
308
+
309
+ Raises:
310
+ RuntimeError: If the client has been destroyed.
311
+ """
312
+ self._ensure_not_destroyed()
313
+
314
+ funcs = get_auth_functions()
315
+ client_set_option = funcs.get("client_set_option")
316
+ if client_set_option is None:
317
+ return False
318
+
319
+ result = client_set_option(
320
+ self._handle,
321
+ key.encode("utf-8"),
322
+ value.encode("utf-8"),
323
+ )
324
+
325
+ return result == GopherAuthError.SUCCESS
326
+
327
+ def validate_token(
328
+ self,
329
+ token: str,
330
+ options: Optional[GopherValidationOptions] = None,
331
+ ) -> ValidationResult:
332
+ """
333
+ Validate a JWT token.
334
+
335
+ Args:
336
+ token: The JWT token string to validate.
337
+ options: Optional validation options (scopes, audience, etc.).
338
+
339
+ Returns:
340
+ ValidationResult with valid flag, error code, and message.
341
+
342
+ Raises:
343
+ RuntimeError: If the client has been destroyed or function unavailable.
344
+ """
345
+ self._ensure_not_destroyed()
346
+
347
+ funcs = get_auth_functions()
348
+ validate_token = funcs.get("validate_token")
349
+ if validate_token is None:
350
+ return ValidationResult(
351
+ valid=False,
352
+ error_code=GopherAuthError.NOT_INITIALIZED,
353
+ error_message="validate_token function not available",
354
+ )
355
+
356
+ result_struct = GopherAuthValidationResult()
357
+ options_handle = options.get_handle() if options else None
358
+
359
+ err = validate_token(
360
+ self._handle,
361
+ token.encode("utf-8"),
362
+ options_handle,
363
+ byref(result_struct),
364
+ )
365
+
366
+ if err != GopherAuthError.SUCCESS:
367
+ return ValidationResult(
368
+ valid=False,
369
+ error_code=err,
370
+ error_message=get_error_description(err),
371
+ )
372
+
373
+ error_message = None
374
+ if result_struct.error_message:
375
+ error_message = result_struct.error_message.decode("utf-8")
376
+
377
+ return ValidationResult(
378
+ valid=result_struct.valid,
379
+ error_code=result_struct.error_code,
380
+ error_message=error_message,
381
+ )
382
+
383
+ def extract_payload(self, token: str) -> Optional[TokenPayload]:
384
+ """
385
+ Extract payload from a JWT token without validation.
386
+
387
+ Args:
388
+ token: The JWT token string.
389
+
390
+ Returns:
391
+ TokenPayload with extracted claims, or None on error.
392
+
393
+ Raises:
394
+ RuntimeError: If the client has been destroyed.
395
+ """
396
+ self._ensure_not_destroyed()
397
+
398
+ funcs = get_auth_functions()
399
+ extract_payload = funcs.get("extract_payload")
400
+ if extract_payload is None:
401
+ return None
402
+
403
+ payload_handle = c_void_p()
404
+ result = extract_payload(
405
+ token.encode("utf-8"),
406
+ byref(payload_handle),
407
+ )
408
+
409
+ if result != GopherAuthError.SUCCESS:
410
+ return None
411
+
412
+ try:
413
+ subject = self._get_payload_string(payload_handle, "payload_get_subject")
414
+ scopes = self._get_payload_string(payload_handle, "payload_get_scopes")
415
+ audience = self._get_payload_string(payload_handle, "payload_get_audience")
416
+ issuer = self._get_payload_string(payload_handle, "payload_get_issuer")
417
+
418
+ # Get expiration
419
+ expiration = None
420
+ get_exp = funcs.get("payload_get_expiration")
421
+ if get_exp:
422
+ exp_value = c_int64()
423
+ if get_exp(payload_handle, byref(exp_value)) == GopherAuthError.SUCCESS:
424
+ expiration = exp_value.value
425
+
426
+ return TokenPayload(
427
+ subject=subject or "",
428
+ scopes=scopes or "",
429
+ audience=audience,
430
+ expiration=expiration,
431
+ issuer=issuer,
432
+ )
433
+ finally:
434
+ # Clean up payload handle
435
+ payload_destroy = funcs.get("payload_destroy")
436
+ if payload_destroy:
437
+ payload_destroy(payload_handle)
438
+
439
+ def _get_payload_string(
440
+ self,
441
+ payload_handle: c_void_p,
442
+ func_name: str,
443
+ ) -> Optional[str]:
444
+ """
445
+ Helper to get a string field from payload.
446
+
447
+ Args:
448
+ payload_handle: The payload handle.
449
+ func_name: Name of the getter function.
450
+
451
+ Returns:
452
+ The string value, or None if unavailable.
453
+ """
454
+ funcs = get_auth_functions()
455
+ getter = funcs.get(func_name)
456
+ if getter is None:
457
+ return None
458
+
459
+ value_out = c_char_p()
460
+ result = getter(payload_handle, byref(value_out))
461
+
462
+ if result != GopherAuthError.SUCCESS:
463
+ return None
464
+
465
+ if value_out.value:
466
+ value = value_out.value.decode("utf-8")
467
+ # Free the allocated string
468
+ free_string = funcs.get("free_string")
469
+ if free_string:
470
+ free_string(value_out)
471
+ return value
472
+
473
+ return None
474
+
475
+ def validate_and_extract(
476
+ self,
477
+ token: str,
478
+ options: Optional[GopherValidationOptions] = None,
479
+ ) -> Tuple[ValidationResult, Optional[TokenPayload]]:
480
+ """
481
+ Validate a token and extract its payload in one call.
482
+
483
+ Args:
484
+ token: The JWT token string.
485
+ options: Optional validation options.
486
+
487
+ Returns:
488
+ Tuple of (ValidationResult, TokenPayload or None).
489
+
490
+ Raises:
491
+ RuntimeError: If the client has been destroyed.
492
+ """
493
+ result = self.validate_token(token, options)
494
+
495
+ if not result.valid:
496
+ return result, None
497
+
498
+ payload = self.extract_payload(token)
499
+ return result, payload
500
+
501
+ def destroy(self) -> None:
502
+ """
503
+ Destroy the client and release resources.
504
+
505
+ This method is idempotent - calling it multiple times is safe.
506
+ """
507
+ if self._destroyed or self._handle is None:
508
+ return
509
+
510
+ funcs = get_auth_functions()
511
+ client_destroy = funcs.get("client_destroy")
512
+ if client_destroy is not None:
513
+ client_destroy(self._handle)
514
+
515
+ self._handle = None
516
+ self._destroyed = True
517
+
518
+ def is_destroyed(self) -> bool:
519
+ """
520
+ Check if this client has been destroyed.
521
+
522
+ Returns:
523
+ True if destroyed, False otherwise.
524
+ """
525
+ return self._destroyed
526
+
527
+ def _ensure_not_destroyed(self) -> None:
528
+ """
529
+ Ensure the client has not been destroyed.
530
+
531
+ Raises:
532
+ RuntimeError: If the client has been destroyed.
533
+ """
534
+ if self._destroyed:
535
+ raise RuntimeError("GopherAuthClient has been destroyed")
536
+
537
+ def __enter__(self) -> "GopherAuthClient":
538
+ """Enter context manager."""
539
+ return self
540
+
541
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
542
+ """Exit context manager and destroy resources."""
543
+ self.destroy()
544
+
545
+ def __del__(self) -> None:
546
+ """Destructor - ensure resources are released."""
547
+ self.destroy()