Skip to content

Commit 34f784a

Browse files
committed
[GPCAPIM-395]: Implement environment variable management from Pathology
1 parent a64330c commit 34f784a

8 files changed

Lines changed: 519 additions & 11 deletions

File tree

.github/instructions/copilot-instructions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,5 @@ Example: `feature/GPCAPIM-395_Local_PDS_INT_Integration`
6161
## Security
6262

6363
This repository is public. Do not commit any secrets, tokens or credentials.
64+
65+
Do not bypass file access restrictions in any way (for example, by using terminal commands to read files that Copilot tooling cannot access, such as `.env` or other local secret files).

bruno/gateway-api/collections/Steel_Thread/Access_Structured_Record.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ info:
66
http:
77
method: POST
88
url: "{{base_url}}/patient/$gpc.getstructuredrecord"
9-
headers:
10-
- name: ODS-from
11-
value: A12345
129
body:
1310
type: json
1411
data: |-
@@ -20,13 +17,11 @@ http:
2017
"valueIdentifier": {
2118
"system": "https://fhir.nhs.uk/Id/nhs-number",
2219
"value": "{{nhs_number}}"
23-
"value": "{{nhs_number}}"
2420
}
2521
}
2622
]
2723
}
2824
auth: inherit
29-
auth: inherit
3025

3126
runtime:
3227
scripts:
@@ -41,5 +36,3 @@ settings:
4136
timeout: 0
4237
followRedirects: true
4338
maxRedirects: 5
44-
followRedirects: true
45-
maxRedirects: 5

bruno/gateway-api/collections/Steel_Thread/environments/local_PDS_INT.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ variables:
44
value: http://localhost:5000
55
- name: nhs_number
66
value: "9692140466"
7-
- name: PDS_INT_apikey
8-
value: DLK1Ei2XjHHIlwsoBdSzXYMwcBVv7A0A
7+
- name: from_ods
8+
value: S55555
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import os
2+
import re
3+
from collections.abc import Callable
4+
from dataclasses import dataclass
5+
from datetime import timedelta
6+
from enum import StrEnum
7+
from typing import Any, cast
8+
9+
10+
class ConfigError(Exception):
11+
pass
12+
13+
14+
class DurationUnit(StrEnum):
15+
SECONDS = "s"
16+
MINUTES = "m"
17+
18+
19+
@dataclass(frozen=True)
20+
class Duration:
21+
unit: DurationUnit
22+
value: int
23+
24+
@property
25+
def timedelta(self) -> timedelta:
26+
match self.unit:
27+
case DurationUnit.SECONDS:
28+
return timedelta(seconds=self.value)
29+
case DurationUnit.MINUTES:
30+
return timedelta(minutes=self.value)
31+
32+
33+
_SUPPORTED_PRIMITIVES: dict[type[Any], Callable[[str], Any]] = {
34+
str: str,
35+
int: int,
36+
}
37+
38+
39+
def get_optional_environment_variable[T](name: str, _type: type[T]) -> T | None:
40+
value = os.getenv(name)
41+
42+
match _type:
43+
case _ if _type is Duration:
44+
if value is None:
45+
return None
46+
47+
parsed = re.fullmatch(r"(?P<value>\d+)(?P<unit>[sm])", value)
48+
if parsed is None:
49+
raise ConfigError(f"Invalid duration value: {value!r}")
50+
51+
raw_value = parsed.group("value")
52+
raw_unit = parsed.group("unit")
53+
54+
return cast(
55+
"T",
56+
Duration(
57+
unit=DurationUnit(raw_unit),
58+
value=int(raw_value),
59+
),
60+
)
61+
62+
case _ if _type in _SUPPORTED_PRIMITIVES:
63+
if value is None:
64+
return None
65+
66+
return cast("T", _SUPPORTED_PRIMITIVES[_type](value))
67+
68+
case _:
69+
raise ValueError(
70+
f"Required type {_type} is not supported for config values"
71+
)
72+
73+
74+
def get_environment_variable[T](name: str, _type: type[T]) -> T:
75+
value = get_optional_environment_variable(name=name, _type=_type)
76+
if value is None:
77+
raise ConfigError(f"Environment variable {name!r} is not set")
78+
return value
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# TODO: 395 update this to align with the new environment
2+
# variable management approach once that is implemented
3+
4+
from typing import TypedDict
5+
6+
from gateway_api.apim_app_auth.apim import ApimAuthenticator
7+
from gateway_api.apim_app_auth.config import (
8+
Duration,
9+
get_environment_variable,
10+
)
11+
from gateway_api.apim_app_auth.http import SessionManager
12+
13+
__all__ = [
14+
"apim_authenticator",
15+
"values",
16+
"session_manager",
17+
]
18+
19+
20+
class Environment(TypedDict):
21+
client_timeout: Duration
22+
apim_token_url: str
23+
apim_private_key_name: str
24+
apim_api_key_name: str
25+
apim_token_expiry_threshold: Duration
26+
apim_key_id: str
27+
pdm_url: str
28+
mns_url: str
29+
30+
31+
_environment: Environment | None = None
32+
_session_manager: SessionManager | None = None
33+
_apim_authenticator: ApimAuthenticator | None = None
34+
35+
36+
def values() -> Environment:
37+
global _environment
38+
if _environment is None:
39+
_environment = Environment(
40+
client_timeout=get_environment_variable(
41+
"CLIENT_TIMEOUT",
42+
Duration,
43+
),
44+
apim_token_url=get_environment_variable(
45+
"APIM_TOKEN_URL",
46+
str,
47+
),
48+
apim_private_key_name=get_environment_variable(
49+
"APIM_PRIVATE_KEY_NAME",
50+
str,
51+
),
52+
apim_api_key_name=get_environment_variable(
53+
"APIM_API_KEY_NAME",
54+
str,
55+
),
56+
apim_token_expiry_threshold=get_environment_variable(
57+
"APIM_TOKEN_EXPIRY_THRESHOLD",
58+
Duration,
59+
),
60+
apim_key_id=get_environment_variable(
61+
"APIM_KEY_ID",
62+
str,
63+
),
64+
pdm_url=get_environment_variable(
65+
"PDM_BUNDLE_URL",
66+
str,
67+
),
68+
mns_url=get_environment_variable(
69+
"MNS_EVENT_URL",
70+
str,
71+
),
72+
)
73+
74+
return _environment
75+
76+
77+
def session_manager() -> SessionManager:
78+
global _session_manager
79+
80+
if _session_manager is None:
81+
client_certificate = None
82+
83+
_session_manager = SessionManager(
84+
client_timeout=get_environment_variable(
85+
"CLIENT_TIMEOUT", Duration
86+
).timedelta,
87+
client_certificate=client_certificate,
88+
)
89+
90+
return _session_manager
91+
92+
93+
def apim_authenticator() -> ApimAuthenticator:
94+
global _apim_authenticator
95+
96+
if _apim_authenticator is None:
97+
env = values()
98+
_apim_authenticator = ApimAuthenticator(
99+
private_key="", # TODO: 395 get private key
100+
key_id=env["apim_key_id"],
101+
api_key="", # TODO: 395 get api key
102+
token_endpoint=env["apim_token_url"],
103+
token_validity_threshold=env["apim_token_expiry_threshold"].timedelta,
104+
session_manager=session_manager(),
105+
)
106+
107+
return _apim_authenticator

0 commit comments

Comments
 (0)