gaard-api 0.2.2__tar.gz → 0.2.3__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 (63) hide show
  1. {gaard_api-0.2.2/src/gaard_api.egg-info → gaard_api-0.2.3}/PKG-INFO +2 -1
  2. {gaard_api-0.2.2 → gaard_api-0.2.3}/pyproject.toml +3 -2
  3. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/core/settings.py +1 -1
  4. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/license.py +4 -3
  5. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/package_updates.py +11 -7
  6. gaard_api-0.2.3/src/gaard_api/py.typed +0 -0
  7. gaard_api-0.2.3/src/gaard_api/tls_http.py +50 -0
  8. {gaard_api-0.2.2 → gaard_api-0.2.3/src/gaard_api.egg-info}/PKG-INFO +2 -1
  9. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api.egg-info/SOURCES.txt +4 -1
  10. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api.egg-info/requires.txt +1 -0
  11. {gaard_api-0.2.2 → gaard_api-0.2.3}/tests/test_license_api.py +13 -0
  12. {gaard_api-0.2.2 → gaard_api-0.2.3}/tests/test_settings.py +1 -0
  13. gaard_api-0.2.3/tests/test_tls_http.py +70 -0
  14. {gaard_api-0.2.2 → gaard_api-0.2.3}/MANIFEST.in +0 -0
  15. {gaard_api-0.2.2 → gaard_api-0.2.3}/README.md +0 -0
  16. {gaard_api-0.2.2 → gaard_api-0.2.3}/setup.cfg +0 -0
  17. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/__init__.py +0 -0
  18. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin/__init__.py +0 -0
  19. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin/database.py +0 -0
  20. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin/defaults.py +0 -0
  21. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin/models.py +0 -0
  22. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin/prompt_runtime.py +0 -0
  23. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin/security.py +0 -0
  24. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin/services.py +0 -0
  25. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin-web/assets/getgaard.svg +0 -0
  26. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin-web/assets/main.js +0 -0
  27. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin-web/assets/styles.css +0 -0
  28. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin-web/index.html +0 -0
  29. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/admin-web/package.json +0 -0
  30. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/api/__init__.py +0 -0
  31. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/api/v1/__init__.py +0 -0
  32. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/api/v1/admin.py +0 -0
  33. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/api/v1/analysis.py +0 -0
  34. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/api/v1/prompts.py +0 -0
  35. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/api/v1/query.py +0 -0
  36. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/api/v1/schema.py +0 -0
  37. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/api_registry.py +0 -0
  38. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/cli.py +0 -0
  39. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/cli_commands.py +0 -0
  40. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/core/__init__.py +0 -0
  41. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/core/error_handlers.py +0 -0
  42. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/core/schema_cache.py +0 -0
  43. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/example_data/__init__.py +0 -0
  44. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
  45. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
  46. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
  47. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/example_database.py +0 -0
  48. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/extension_services.py +0 -0
  49. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/extensions.py +0 -0
  50. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/main.py +0 -0
  51. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/query_hooks.py +0 -0
  52. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api/server_cli.py +0 -0
  53. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api.egg-info/dependency_links.txt +0 -0
  54. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api.egg-info/entry_points.txt +0 -0
  55. {gaard_api-0.2.2 → gaard_api-0.2.3}/src/gaard_api.egg-info/top_level.txt +0 -0
  56. {gaard_api-0.2.2 → gaard_api-0.2.3}/tests/test_admin_api.py +0 -0
  57. {gaard_api-0.2.2 → gaard_api-0.2.3}/tests/test_analysis_api.py +0 -0
  58. {gaard_api-0.2.2 → gaard_api-0.2.3}/tests/test_api_registry.py +0 -0
  59. {gaard_api-0.2.2 → gaard_api-0.2.3}/tests/test_error_handlers.py +0 -0
  60. {gaard_api-0.2.2 → gaard_api-0.2.3}/tests/test_license_service.py +0 -0
  61. {gaard_api-0.2.2 → gaard_api-0.2.3}/tests/test_prompt_runtime.py +0 -0
  62. {gaard_api-0.2.2 → gaard_api-0.2.3}/tests/test_query_error_messages.py +0 -0
  63. {gaard_api-0.2.2 → gaard_api-0.2.3}/tests/test_server_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gaard-api
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: GAARD backend web services providing admin interface
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -13,6 +13,7 @@ Requires-Dist: uvicorn[standard]>=0.30.0
13
13
  Requires-Dist: pydantic>=2.7.0
14
14
  Requires-Dist: sqlalchemy>=2.0.0
15
15
  Requires-Dist: httpx2>=2.5.0
16
+ Requires-Dist: certifi>=2024.8.30
16
17
  Provides-Extra: oracle
17
18
  Requires-Dist: oracledb>=2.0.0; extra == "oracle"
18
19
  Provides-Extra: mssql
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gaard-api"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "GAARD backend web services providing admin interface"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -18,7 +18,8 @@ dependencies = [
18
18
  "uvicorn[standard]>=0.30.0",
19
19
  "pydantic>=2.7.0",
20
20
  "sqlalchemy>=2.0.0",
21
- "httpx2>=2.5.0"
21
+ "httpx2>=2.5.0",
22
+ "certifi>=2024.8.30"
22
23
  ]
23
24
 
24
25
  [project.optional-dependencies]
@@ -126,7 +126,7 @@ class Settings:
126
126
  )
127
127
  )
128
128
  gaard_package_directory: str = field(
129
- default_factory=lambda: env_value("GAARD_PACKAGE_DIRECTORY", "packages")
129
+ default_factory=lambda: env_value("GAARD_PACKAGE_DIRECTORY", "extensions")
130
130
  )
131
131
  gaard_package_install_timeout_seconds: int = field(
132
132
  default_factory=lambda: env_int_value(
@@ -16,6 +16,7 @@ from gaard_core.errors import GaardError
16
16
 
17
17
  from gaard_api.admin.models import AdminSetting, DatasourceConnector
18
18
  from gaard_api.core.settings import settings
19
+ from gaard_api.tls_http import http_error_summary, post as tls_post
19
20
 
20
21
 
21
22
  logger = logging.getLogger(__name__)
@@ -266,7 +267,7 @@ class LicenseService:
266
267
  self._stop_event = threading.Event()
267
268
  self._thread: threading.Thread | None = None
268
269
  self._consecutive_transient_failures = 0
269
- self._http_post = httpx.post
270
+ self._http_post = tls_post
270
271
 
271
272
  @property
272
273
  def state(self) -> LicenseState:
@@ -281,7 +282,7 @@ class LicenseService:
281
282
  with self._state_lock:
282
283
  self._state = community_state()
283
284
  self._consecutive_transient_failures = 0
284
- self._http_post = httpx.post
285
+ self._http_post = tls_post
285
286
 
286
287
  def start(self) -> None:
287
288
  self.refresh(force=True)
@@ -595,7 +596,7 @@ class LicenseService:
595
596
  except httpx.HTTPError as exc:
596
597
  return LicenseValidationResult(
597
598
  kind="transient",
598
- message=f"Online license validation failed: {exc.__class__.__name__}.",
599
+ message=f"Online license validation failed: {http_error_summary(exc)}.",
599
600
  )
600
601
 
601
602
  status_code = int(response.status_code)
@@ -28,6 +28,8 @@ from packaging.version import InvalidVersion, Version
28
28
 
29
29
  from gaard_api.core.settings import settings
30
30
  from gaard_api.license import LicensePlan, LicenseState, gaard_api_version
31
+ from gaard_api.tls_http import get as tls_get
32
+ from gaard_api.tls_http import http_error_summary, post as tls_post
31
33
 
32
34
 
33
35
  logger = logging.getLogger(__name__)
@@ -190,15 +192,15 @@ def default_pip_runner(
190
192
 
191
193
  class PackageUpdateService:
192
194
  def __init__(self) -> None:
193
- self._http_post: HttpRequest = httpx.post
194
- self._http_get: HttpRequest = httpx.get
195
+ self._http_post: HttpRequest = tls_post
196
+ self._http_get: HttpRequest = tls_get
195
197
  self._package_version: PackageVersionResolver = version
196
198
  self._pip_runner: PipRunner = default_pip_runner
197
199
  self._lock = threading.Lock()
198
200
 
199
201
  def reset_for_tests(self) -> None:
200
- self._http_post = httpx.post
201
- self._http_get = httpx.get
202
+ self._http_post = tls_post
203
+ self._http_get = tls_get
202
204
  self._package_version = version
203
205
  self._pip_runner = default_pip_runner
204
206
 
@@ -442,7 +444,8 @@ class PackageUpdateService:
442
444
  )
443
445
  except httpx.HTTPError as exc:
444
446
  raise PackageUpdateError(
445
- f"Package download failed before receiving a response: {exc.__class__.__name__}."
447
+ "Package download failed before receiving a response: "
448
+ f"{http_error_summary(exc)}."
446
449
  ) from exc
447
450
 
448
451
  status_code = int(response.status_code)
@@ -575,7 +578,8 @@ class PackageUpdateService:
575
578
  )
576
579
  except httpx.HTTPError as exc:
577
580
  raise PackageUpdateError(
578
- f"Package file download failed before receiving a response: {exc.__class__.__name__}."
581
+ "Package file download failed before receiving a response: "
582
+ f"{http_error_summary(exc)}."
579
583
  ) from exc
580
584
 
581
585
  status_code = int(response.status_code)
@@ -894,7 +898,7 @@ class PackageUpdateService:
894
898
  }
895
899
 
896
900
  def _package_root(self) -> Path:
897
- configured = str(settings.gaard_package_directory or "packages").strip() or "packages"
901
+ configured = str(settings.gaard_package_directory or "extensions").strip() or "extensions"
898
902
  path = Path(configured)
899
903
  if not path.is_absolute():
900
904
  path = Path.cwd() / path
File without changes
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ import os
5
+ import ssl
6
+ from typing import Any
7
+ from urllib.parse import urlparse
8
+
9
+ import certifi
10
+ import httpx2 as httpx
11
+
12
+
13
+ def post(url: str, **kwargs: Any) -> httpx.Response:
14
+ return request_with_certifi_fallback(httpx.post, url, **kwargs)
15
+
16
+
17
+ def get(url: str, **kwargs: Any) -> httpx.Response:
18
+ return request_with_certifi_fallback(httpx.get, url, **kwargs)
19
+
20
+
21
+ def request_with_certifi_fallback(request: Any, url: str, **kwargs: Any) -> httpx.Response:
22
+ try:
23
+ return request(url, **kwargs)
24
+ except httpx.ConnectError as exc:
25
+ if not should_retry_with_certifi(url, kwargs):
26
+ raise
27
+ try:
28
+ return request(url, **{**kwargs, "verify": certifi_ssl_context()})
29
+ except httpx.HTTPError as fallback_exc:
30
+ raise fallback_exc from exc
31
+
32
+
33
+ def should_retry_with_certifi(url: str, kwargs: dict[str, Any]) -> bool:
34
+ if kwargs.get("verify") is not None:
35
+ return False
36
+ if os.environ.get("SSL_CERT_FILE") or os.environ.get("SSL_CERT_DIR"):
37
+ return False
38
+ return urlparse(url).scheme == "https"
39
+
40
+
41
+ @lru_cache(maxsize=1)
42
+ def certifi_ssl_context() -> ssl.SSLContext:
43
+ return ssl.create_default_context(cafile=certifi.where())
44
+
45
+
46
+ def http_error_summary(exc: httpx.HTTPError) -> str:
47
+ detail = str(exc).strip()
48
+ if detail:
49
+ return f"{exc.__class__.__name__}: {detail}"
50
+ return exc.__class__.__name__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gaard-api
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: GAARD backend web services providing admin interface
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -13,6 +13,7 @@ Requires-Dist: uvicorn[standard]>=0.30.0
13
13
  Requires-Dist: pydantic>=2.7.0
14
14
  Requires-Dist: sqlalchemy>=2.0.0
15
15
  Requires-Dist: httpx2>=2.5.0
16
+ Requires-Dist: certifi>=2024.8.30
16
17
  Provides-Extra: oracle
17
18
  Requires-Dist: oracledb>=2.0.0; extra == "oracle"
18
19
  Provides-Extra: mssql
@@ -11,8 +11,10 @@ src/gaard_api/extensions.py
11
11
  src/gaard_api/license.py
12
12
  src/gaard_api/main.py
13
13
  src/gaard_api/package_updates.py
14
+ src/gaard_api/py.typed
14
15
  src/gaard_api/query_hooks.py
15
16
  src/gaard_api/server_cli.py
17
+ src/gaard_api/tls_http.py
16
18
  src/gaard_api.egg-info/PKG-INFO
17
19
  src/gaard_api.egg-info/SOURCES.txt
18
20
  src/gaard_api.egg-info/dependency_links.txt
@@ -55,4 +57,5 @@ tests/test_license_service.py
55
57
  tests/test_prompt_runtime.py
56
58
  tests/test_query_error_messages.py
57
59
  tests/test_server_cli.py
58
- tests/test_settings.py
60
+ tests/test_settings.py
61
+ tests/test_tls_http.py
@@ -7,6 +7,7 @@ uvicorn[standard]>=0.30.0
7
7
  pydantic>=2.7.0
8
8
  sqlalchemy>=2.0.0
9
9
  httpx2>=2.5.0
10
+ certifi>=2024.8.30
10
11
 
11
12
  [all-datasources]
12
13
  oracledb>=2.0.0
@@ -287,6 +287,19 @@ def test_package_update_requires_paid_license(license_client: TestClient) -> Non
287
287
  assert response.json()["error"]["code"] == "LICENSE_ENTITLEMENT_REQUIRED"
288
288
 
289
289
 
290
+ def test_package_updates_default_to_extensions_directory(
291
+ tmp_path: Path,
292
+ monkeypatch: pytest.MonkeyPatch,
293
+ ) -> None:
294
+ monkeypatch.chdir(tmp_path)
295
+ monkeypatch.setattr(settings, "gaard_package_directory", "")
296
+
297
+ package_root = package_update_service._package_root()
298
+
299
+ assert package_root == tmp_path / "extensions"
300
+ assert package_root.is_dir()
301
+
302
+
290
303
  def test_paid_license_can_download_extract_and_install_package_updates(
291
304
  license_client: TestClient,
292
305
  tmp_path: Path,
@@ -13,6 +13,7 @@ def test_settings_use_llm_defaults_and_ignore_metadata_env(monkeypatch) -> None:
13
13
  assert settings.gaard_sql_generation_mode == "llm"
14
14
  assert settings.gaard_result_interpretation_mode == "llm"
15
15
  assert settings.gaard_intent_classification_mode == "auto"
16
+ assert settings.gaard_package_directory == "extensions"
16
17
 
17
18
 
18
19
  def test_settings_can_seed_runtime_defaults_from_process_env(monkeypatch) -> None:
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import ssl
4
+
5
+ import httpx2 as httpx
6
+ import pytest
7
+
8
+ from gaard_api.tls_http import request_with_certifi_fallback
9
+
10
+
11
+ def test_https_connect_error_retries_with_certifi_context() -> None:
12
+ calls: list[dict] = []
13
+
14
+ def fake_request(url: str, **kwargs):
15
+ calls.append(kwargs)
16
+ if len(calls) == 1:
17
+ raise httpx.ConnectError(("certificate is not trusted",))
18
+ return httpx.Response(200, json={"ok": True})
19
+
20
+ response = request_with_certifi_fallback(
21
+ fake_request,
22
+ "https://getgaard.com/api/license/validate",
23
+ json={"product": "gaard"},
24
+ timeout=10.0,
25
+ )
26
+
27
+ assert response.status_code == 200
28
+ assert "verify" not in calls[0]
29
+ assert isinstance(calls[1]["verify"], ssl.SSLContext)
30
+
31
+
32
+ def test_connect_error_does_not_retry_when_verify_is_explicit() -> None:
33
+ calls = 0
34
+
35
+ def fake_request(url: str, **kwargs):
36
+ nonlocal calls
37
+ calls += 1
38
+ raise httpx.ConnectError(("certificate is not trusted",))
39
+
40
+ context = ssl.create_default_context()
41
+ with pytest.raises(httpx.ConnectError):
42
+ request_with_certifi_fallback(
43
+ fake_request,
44
+ "https://getgaard.com/api/license/validate",
45
+ verify=context,
46
+ timeout=10.0,
47
+ )
48
+
49
+ assert calls == 1
50
+
51
+
52
+ def test_connect_error_does_not_retry_when_ssl_cert_file_is_configured(
53
+ monkeypatch: pytest.MonkeyPatch,
54
+ ) -> None:
55
+ calls = 0
56
+
57
+ def fake_request(url: str, **kwargs):
58
+ nonlocal calls
59
+ calls += 1
60
+ raise httpx.ConnectError(("certificate is not trusted",))
61
+
62
+ monkeypatch.setenv("SSL_CERT_FILE", "/custom/ca.pem")
63
+ with pytest.raises(httpx.ConnectError):
64
+ request_with_certifi_fallback(
65
+ fake_request,
66
+ "https://getgaard.com/api/license/validate",
67
+ timeout=10.0,
68
+ )
69
+
70
+ assert calls == 1
File without changes
File without changes
File without changes