-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/gpcapim 251 #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
a73f501 to
89fa638
Compare
Vox-Ben
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, comments mostly on non-critical stuff. :-)
| import requests | ||
| from requests import Response |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to do this? You're basically importing from the same file twice (which AI seems to like doing and I'm not sure why). IMO better to just do from requests import Response, <whatever else you're using>.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, this cleans things up a bit, thanks
| from requests import Response | ||
|
|
||
| # definitions | ||
| ars_interactionId = "urn:nhs:names:services:gpconnect:structured:fhir:operation:gpc.getstructuredrecord-1" # noqa: E501 this is standard InteractionID for accessRecordStructured |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can break this over two lines, it doesn't need to bust the line length limit.
ars_interactionId = ("urn:nhs:names:services:gpconnect:structured"
":fhir:operation:gpc.getstructuredrecord-1")
| This exception wraps :class:`requests.HTTPError` thrown by | ||
| ``response.raise_for_status()`` and re-raises it as ``ExternalServiceError`` | ||
| to decouple callers from the underlying ``requests`` library exception types. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this comment. The exception as written isn't related to requests.HTTPError and doesn't have any of its own logic. I'd just delete this bit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment is not saying it inherits; it re-raises. Obfuscating the actual error to the end user. Line 137 onwards:
except requests.HTTPError as err:
raise ExternalServiceError(
f"GPProvider FHIR API request failed:{err.response.reason}"
) from err
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're likely to move this to a common library then I'll just delete it from here and we can agree wording when we do that.
| timeout = None # None used for quicker dev, adjust as needed | ||
|
|
||
|
|
||
| class ExternalServiceError(Exception): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could move this into a common library. I've started one in my current ticket anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed - happy to pattern here, or integrate this as we merge/rebase
| Args: | ||
| provider_endpoint (str): The FHIR API endpoint for the provider. | ||
| provider_asid (str): The ASID for the provider. | ||
| consumer_asid (str): The ASID for the consumer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest we write docstrings with Sphinx-style argument listing. Then we can auto-generate our documentation (though that said I don't know if the likes of ChatGPT are going to obsolete the likes of Sphinx). Still, it's standard and it gives us the option, and it's easy enough to just ask the AI to do. Don't know if @davidhamill1-nhs and @nhsd-jack-wainwright agree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a starter for ten, I'm okay with this.
My preference is to avoid primitives in the parameters, and define ProviderEndpoint, etc. types some other way (rather than saying they can be any string). By doing so, your type hinting is your documentation.
I assume any LLM running over the codebase will correctly pull the type and any validations/restrictions, and thus you can still gain the AI's summary of the code.
But happy to go with the consensus.
| headers = self._build_headers(trace_id) | ||
|
|
||
| response = requests.post( | ||
| self.provider_endpoint + ars_fhir_base + "/patient/" + ars_fhir_operation, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was convinced that Python had a built-in function for constructing URLs from arbitrary components, but it turns out that it doesn't. And you can't use pathlib either because if any component has a leading slash it drops all the previous components. I wonder if it's worth writing one ourselves.
| Fixtures: | ||
| - `stub`: Provides an instance of the `GpProviderStub` for simulating the GPProvider | ||
| - `mock_request_post`: Patches the `requests.post` method to intercept API calls and | ||
| route them to the stub provider. Captures request details for verification. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure we really need to list the fixtures in the module docstring. Easy enough to find them by looking through the file, and the header will go out of date because nobody will update it.
|
|
||
| from gateway_api.provider_request import ExternalServiceError, GpProviderClient | ||
|
|
||
| ars_interactionId = "urn:nhs:names:services:gpconnect:structured:fhir:operation:gpc.getstructuredrecord-1" # noqa: E501 this is standard InteractionID for accessRecordStructured |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above don't need to #noqa this, just break it across two lines.
- Create GPProviderClient class to interact with GPProvider FHIR API - Add initialization method with provider and consumer ASIDs - Implement get_structured_record method to fetch patient records - Update unit tests to verify successful response from GPProviderClient from example.com
- Change return type of get_structured_record method to Response - Update timeout parameter to None for quicker development - Add mock_request_get fixture to unit tests for simulating API responses
- Rename method `get_structured_record` to `access_structured_record` - Change HTTP method from GET to POST for fetching structured patient records - Update request headers to match GP Connect specifications - Enhance mock request handling in tests for POST requests - Add a GPProviderStub class to simulate structured patient record responses
- Correct header casing in GPProviderClient for FHIR API requests - Enhance GPProviderStub to return a Response object - Refactor mock_request_post to utilize GPProviderStub
…ap to https for dummy url
…PI requests - Implement _build_headers method to construct request headers - Update access_structured_record method to utilize new headers - Modify unit tests to verify correct headers are sent in requests
- Modify access_structured_record method to accept a body parameter - Update request handling to include body in POST requests - Add unit test to verify correct body is sent in requests
- Implement test to verify GPProviderClient returns correct response from stub provider - Remove outdated pseudo-code comments from test file - Update stub provider response handling for initial implementation
- Introduce a StubResponse class for simplified response representation - Update access_record_structured method to return StubResponse instead of Response - Enhance test cases to utilize FakeResponse for better testing of HTTP interactions
- Replace FakeResponse with a more generic StubResponse class - Update mock_request_post to return the actual stub response - Adjust content handling in tests to match new response structure
- Update interaction ID and base URL definitions for clarity - Adjust request timeout handling for better development flexibility - Enhance unit tests to verify correct URL formation and response status
- Implement test to verify ExternalServiceError is raised on HTTP error - Simulate a 500 Internal Server Error response from the GPProvider API - Enhance test coverage for GpProviderClient error handling
2da189a to
2026888
Compare
|
ALB Target: |
davidhamill1-nhs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy with functionality. Some minor comments about style
Same comment as I left with @Vox-Ben , which needs a discussion amongst the team: to what extent do we document in code?
|
|
||
| headers = self._build_headers(trace_id) | ||
|
|
||
| endpoint_path = "/".join([ars_fhir_base, fhir_resource, ars_fhir_operation]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I would say it is easier to parse ...
| endpoint_path = "/".join([ars_fhir_base, fhir_resource, ars_fhir_operation]) | |
| endpoint_path = f"{ars_fhir_base}/{fhir_resource}/{ars_fhir_operation}" |
| ars_interactionId = ( | ||
| "urn:nhs:names:services:gpconnect:structured" | ||
| ":fhir:operation:gpc.getstructuredrecord-1" | ||
| ) | ||
| ars_fhir_base = "FHIR/STU3" | ||
| fhir_resource = "patient" | ||
| ars_fhir_operation = "$gpc.getstructuredrecord" | ||
| timeout = None # None used for quicker dev, adjust as needed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given these are constants, I would suggest making them UPPERCASE. It makes it clearer in the code later on that that the timeout is (currently) a constant.
| """ | ||
| provider_asid = "200000001154" | ||
| consumer_asid = "200000001152" | ||
| provider_endpoint = "https://invalid.com" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Given the assigned string, I would assume this to be an invalid url.
| provider_endpoint = "https://invalid.com" | |
| provider_endpoint = "https://test.com" |
| for key, value in expected_headers.items(): | ||
| assert captured_headers.get(key) == value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless you're expecting the expected headers to be a subset of the captured headers, pytest can "intelligently" assert equality of dicts.
| for key, value in expected_headers.items(): | |
| assert captured_headers.get(key) == value | |
| assert expected_headers == captured_headers |
| from requests.structures import CaseInsensitiveDict | ||
|
|
||
|
|
||
| @dataclass() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason you need this to be a dataclass as well as a Response?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this was left-overs so not needed
|
ALB Target: |
|
ALB Target: |
- Introduced GpProviderStub to simulate provider responses - Updated post method to use stub for structured record access - Enhanced StubResponse to set encoding to UTF-8
|
ALB Target: |
|



Description
This pull request introduces the GpProviderClient class, which provides a simple client for interacting with the GPProvider FHIR GP System. The client includes methods to fetch structured patient records from a GPProvider FHIR API endpoint. Additionally, unit tests and a stub for the GPProvider FHIR API have been implemented to ensure the correctness of the client.
Context
This change is required to enable integration with the GPProvider FHIR GP System, allowing the retrieval of structured patient records. It addresses the need for a robust and reusable client to interact with the GPProvider API, ensuring compliance with the GPConnect specifications.
Type of changes
Checklist
Sensitive Information Declaration
To ensure the utmost confidentiality and protect your and others privacy, we kindly ask you to NOT including PII (Personal Identifiable Information) / PID (Personal Identifiable Data) or any other sensitive data in this PR (Pull Request) and the codebase changes. We will remove any PR that do contain any sensitive information. We really appreciate your cooperation in this matter.