From 734733a3322a6f61e6d84b1f7764d4b609fabefe Mon Sep 17 00:00:00 2001 From: David Graham Date: Tue, 4 Jun 2024 16:53:25 -0400 Subject: [PATCH 1/7] move rcrasite service to file module instead of a directory --- .../rcrasite/{services/rcra_site_services.py => services.py} | 0 server/apps/rcrasite/services/__init__.py | 1 - server/apps/rcrasite/views.py | 3 +-- 3 files changed, 1 insertion(+), 3 deletions(-) rename server/apps/rcrasite/{services/rcra_site_services.py => services.py} (100%) delete mode 100644 server/apps/rcrasite/services/__init__.py diff --git a/server/apps/rcrasite/services/rcra_site_services.py b/server/apps/rcrasite/services.py similarity index 100% rename from server/apps/rcrasite/services/rcra_site_services.py rename to server/apps/rcrasite/services.py diff --git a/server/apps/rcrasite/services/__init__.py b/server/apps/rcrasite/services/__init__.py deleted file mode 100644 index c472030e..00000000 --- a/server/apps/rcrasite/services/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .rcra_site_services import RcraSiteService diff --git a/server/apps/rcrasite/views.py b/server/apps/rcrasite/views.py index a0060b50..e7c2e3db 100644 --- a/server/apps/rcrasite/views.py +++ b/server/apps/rcrasite/views.py @@ -12,8 +12,7 @@ from apps.rcrasite.models import RcraSite from apps.rcrasite.serializers import RcraSiteSearchSerializer, RcraSiteSerializer -from apps.rcrasite.services import RcraSiteService -from apps.rcrasite.services.rcra_site_services import query_rcra_sites +from apps.rcrasite.services import RcraSiteService, query_rcra_sites logger = logging.getLogger(__name__) From 66c2a0c136675c67b8cc0f28ea62118a704157da Mon Sep 17 00:00:00 2001 From: David Graham Date: Tue, 4 Jun 2024 19:16:30 -0400 Subject: [PATCH 2/7] unit test for HandlerSearchView, returns 400 if siteId is shorter than 2 characters --- server/apps/rcrasite/tests/test_views.py | 60 +++++++++++++++++++++++- server/apps/rcrasite/views.py | 5 +- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/server/apps/rcrasite/tests/test_views.py b/server/apps/rcrasite/tests/test_views.py index 7b9e8dfb..98f7da5c 100644 --- a/server/apps/rcrasite/tests/test_views.py +++ b/server/apps/rcrasite/tests/test_views.py @@ -1,10 +1,12 @@ +from unittest.mock import patch + import pytest from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient, APIRequestFactory, force_authenticate from apps.rcrasite.models import RcraSiteType # type: ignore -from apps.rcrasite.views import RcraSiteSearchView # type: ignore +from apps.rcrasite.views import HandlerSearchView, RcraSiteSearchView # type: ignore class TestRcraSiteView: @@ -102,3 +104,59 @@ def test_endpoint_returns_200_if_bad_query_params( # Assert assert response.status_code == status.HTTP_200_OK assert response.headers["Content-Type"] == "application/json" + + +# class TestHandlerSearchView: +# +# @patch("apps.rcrasite.services.RcraSiteService.search_rcrainfo_handlers") +# def test_endpoint_returns_404_if_no_epa_id(self, client): +# response: Response = client.get("/api/rcra/handler/") +# + + +class TestHandlerSearchView: + base_url = "/api/site/search" + + @pytest.fixture(autouse=True) + def set_up(self): + self.view = HandlerSearchView() + self.request_factory = APIRequestFactory() + + def test_valid_search_returns_200(self, user_factory): + with patch("apps.rcrasite.views.RcraSiteService") as MockRcraSiteService: + mock_service = MockRcraSiteService.return_value + mock_service.search_rcrainfo_handlers.return_value = {"sites": []} + user = user_factory() + data = {"siteId": "VAT000000000", "siteType": "designatedFacility"} + request = self.request_factory.post( + f"{self.base_url}", + data=data, + format="json", + ) + force_authenticate(request, user) + response = HandlerSearchView.as_view()(request) + assert response.status_code == status.HTTP_200_OK + + def test_short_epa_id_returns_400(self, user_factory): + user = user_factory() + data = {"siteId": "V", "siteType": "designatedFacility"} + request = self.request_factory.post( + f"{self.base_url}", + data=data, + format="json", + ) + force_authenticate(request, user) + response = HandlerSearchView.as_view()(request) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_null_epa_id_returns_400(self, user_factory): + user = user_factory() + data = {"siteType": "designatedFacility"} + request = self.request_factory.post( + f"{self.base_url}", + data=data, + format="json", + ) + force_authenticate(request, user) + response = HandlerSearchView.as_view()(request) + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/server/apps/rcrasite/views.py b/server/apps/rcrasite/views.py index e7c2e3db..68bf6b67 100644 --- a/server/apps/rcrasite/views.py +++ b/server/apps/rcrasite/views.py @@ -95,7 +95,10 @@ class HandlerSearchView(APIView): """Search and return a list of Hazardous waste handlers from RCRAInfo.""" class HandlerSearchSerializer(serializers.Serializer): - siteId = serializers.CharField(required=True) + siteId = serializers.CharField( + required=True, + min_length=2, + ) siteType = serializers.ChoiceField( required=True, choices=[ From fd14a4386f0764831263d90b09bd2435811c9617 Mon Sep 17 00:00:00 2001 From: David Graham Date: Tue, 4 Jun 2024 19:41:02 -0400 Subject: [PATCH 3/7] refactor 'RcrainfoService' to 'RcraClient' --- server/apps/core/services/__init__.py | 2 +- server/apps/core/services/rcrainfo_service.py | 16 ++++++++-------- .../apps/core/tests/test_rcrainfo_service.py | 18 +++++++++--------- .../handler/tests/test_handler_services.py | 8 ++++---- server/apps/manifest/services/emanifest.py | 8 ++++---- .../apps/manifest/services/emanifest_search.py | 8 ++++---- server/apps/manifest/tasks.py | 4 ++-- server/apps/manifest/tests/conftest.py | 15 ++++++++++++++- .../manifest/tests/test_emanifest_service.py | 6 +++--- .../manifest/tests/test_search_emanifest.py | 8 ++++---- server/apps/profile/services.py | 6 +++--- server/apps/rcrasite/services/__init__.py | 1 + .../{services.py => services/rcra_site.py} | 12 +++--------- server/apps/wasteline/tasks.py | 4 ++-- 14 files changed, 62 insertions(+), 54 deletions(-) create mode 100644 server/apps/rcrasite/services/__init__.py rename server/apps/rcrasite/{services.py => services/rcra_site.py} (91%) diff --git a/server/apps/core/services/__init__.py b/server/apps/core/services/__init__.py index 263809d8..c9d12019 100644 --- a/server/apps/core/services/__init__.py +++ b/server/apps/core/services/__init__.py @@ -1,2 +1,2 @@ -from .rcrainfo_service import RcrainfoService, get_rcrainfo_client +from .rcrainfo_service import RcraClient, get_rcra_client from .task_service import TaskService, get_task_status, launch_example_task diff --git a/server/apps/core/services/rcrainfo_service.py b/server/apps/core/services/rcrainfo_service.py index 70bcf766..7ec4242a 100644 --- a/server/apps/core/services/rcrainfo_service.py +++ b/server/apps/core/services/rcrainfo_service.py @@ -12,9 +12,9 @@ logger = logging.getLogger(__name__) -class RcrainfoService(RcrainfoClient): +class RcraClient(RcrainfoClient): """ - RcrainfoService is our IO interface for communicating with the EPA RCRAInfo + RcraClient is our IO interface for communicating with the EPA RCRAInfo web services. """ @@ -97,22 +97,22 @@ def sign_manifest(self, **sign_data): def __bool__(self): """ This Overrides the RcrainfoClient bool - we use this to test a RcrainfoService instance is not None + we use this to test a RcraClient instance is not None """ return True -def get_rcrainfo_client( +def get_rcra_client( *, username: Optional[str] = None, api_id: Optional[str] = None, api_key: Optional[str] = None, rcrainfo_env: Optional[Literal["preprod"] | Literal["prod"]] = None, **kwargs, -) -> RcrainfoService: - """RcrainfoService Constructor for interacting with RCRAInfo web services""" +) -> RcraClient: + """RcraClient Constructor for interacting with RCRAInfo web services""" if api_id is not None and api_key is not None: - return RcrainfoService( + return RcraClient( api_id=api_id, api_key=api_key, rcrainfo_env=rcrainfo_env, @@ -122,7 +122,7 @@ def get_rcrainfo_client( org: Org = Org.objects.get_by_username(username) if org.is_rcrainfo_integrated: api_id, api_key = org.rcrainfo_api_id_key - return RcrainfoService( + return RcraClient( api_id=api_id, api_key=api_key, rcrainfo_env=rcrainfo_env, diff --git a/server/apps/core/tests/test_rcrainfo_service.py b/server/apps/core/tests/test_rcrainfo_service.py index 460931c7..24c370e0 100644 --- a/server/apps/core/tests/test_rcrainfo_service.py +++ b/server/apps/core/tests/test_rcrainfo_service.py @@ -4,27 +4,27 @@ from responses import matchers from rest_framework import status -from apps.core.services import RcrainfoService, get_rcrainfo_client +from apps.core.services import RcraClient, get_rcra_client from apps.handler.models import QuickerSign from apps.handler.serializers import QuickerSignSerializer class TestRcrainfoService: - """Tests the for the RcrainfoService class""" + """Tests the for the RcraClient class""" def test_inits_to_correct_environment(self): - rcrainfo = RcrainfoService(rcrainfo_env="preprod") + rcrainfo = RcraClient(rcrainfo_env="preprod") assert rcrainfo.rcrainfo_env == "preprod" def test_inits_base_url_by_env(self): - rcrainfo_preprod = RcrainfoService(rcrainfo_env="preprod") + rcrainfo_preprod = RcraClient(rcrainfo_env="preprod") assert rcrainfo_preprod.base_url == emanifest.RCRAINFO_PREPROD - rcrainfo_prod = RcrainfoService(rcrainfo_env="prod") + rcrainfo_prod = RcraClient(rcrainfo_env="prod") assert rcrainfo_prod.base_url == emanifest.RCRAINFO_PROD def test_uses_provided_rcra_profile_credentials(self, rcrainfo_profile_factory): admin_rcrainfo_profile = rcrainfo_profile_factory() - rcrainfo = RcrainfoService(rcra_profile=admin_rcrainfo_profile, auto_renew=True) + rcrainfo = RcraClient(rcra_profile=admin_rcrainfo_profile, auto_renew=True) assert rcrainfo.retrieve_key() == admin_rcrainfo_profile.rcra_api_key assert rcrainfo.retrieve_id() == admin_rcrainfo_profile.rcra_api_id @@ -52,7 +52,7 @@ def test_constructor_retrieves_org_api_credentials( org_access_factory(user=my_user, org=my_org) profile_factory(user=my_user) # Act - rcrainfo: RcrainfoService = get_rcrainfo_client(username=my_user.username) + rcrainfo: RcraClient = get_rcra_client(username=my_user.username) # Assert assert rcrainfo.has_rcrainfo_credentials @@ -63,7 +63,7 @@ def test_constructor_uses_provided_api_credentials( mock_api_id = "my_mock_id" mock_api_key = "my_mock_key" # Act - rcrainfo: RcrainfoService = get_rcrainfo_client(api_id=mock_api_id, api_key=mock_api_key) + rcrainfo: RcraClient = get_rcra_client(api_id=mock_api_id, api_key=mock_api_key) # Assert assert rcrainfo.has_rcrainfo_credentials assert rcrainfo.api_id == mock_api_id @@ -94,7 +94,7 @@ def test_maps_keywords( testuser1 = user_factory() rcra_profile = rcrainfo_profile_factory() profile_factory(user=testuser1, rcrainfo_profile=rcra_profile) - rcrainfo = RcrainfoService(auto_renew=False) + rcrainfo = RcraClient(auto_renew=False) quicker_sign_response_factory(mtn=self.mtn, site_id=self.site_id, sign_date=self.sign_date) quicker_sign_url = f"{rcrainfo.base_url}v1/emanifest/manifest/quicker-sign" mock_responses.post( diff --git a/server/apps/handler/tests/test_handler_services.py b/server/apps/handler/tests/test_handler_services.py index 29b62f97..d975b2b8 100644 --- a/server/apps/handler/tests/test_handler_services.py +++ b/server/apps/handler/tests/test_handler_services.py @@ -1,6 +1,6 @@ import pytest -from apps.core.services import RcrainfoService +from apps.core.services import RcraClient from apps.rcrasite.models import RcraSite from apps.rcrasite.services import RcraSiteService @@ -17,17 +17,17 @@ def _setup(self, user_factory, rcrainfo_profile_factory, haztrak_json, profile_f def test_rcrainfo_returns_true_when_instance(self): """ the e-Manifest PyPI package RcrainfoClient uses a __bool__ method we need to override - in order for self.rcrainfo = rcrainfo or RcrainfoService(...) to work. + in order for self.rcrainfo = rcrainfo or RcraClient(...) to work. """ # Arrange - rcrainfo = RcrainfoService(auto_renew=False) + rcrainfo = RcraClient(auto_renew=False) # Act and Assert assert rcrainfo # should return true def test_pulls_site_details_from_rcrainfo(self, mock_responses): """test pulling a rcra_site's information from rcrainfo""" # Arrange - rcrainfo = RcrainfoService(auto_renew=False) + rcrainfo = RcraClient(auto_renew=False) handler_service = RcraSiteService(username=self.user.username, rcrainfo=rcrainfo) rcrainfo_site_details_url = f"{rcrainfo.base_url}v1/site-details/{self.epa_id}" # mock response from Rcrainfo diff --git a/server/apps/manifest/services/emanifest.py b/server/apps/manifest/services/emanifest.py index 989d399b..db9d638b 100644 --- a/server/apps/manifest/services/emanifest.py +++ b/server/apps/manifest/services/emanifest.py @@ -7,7 +7,7 @@ from emanifest import RcrainfoResponse from requests import RequestException -from apps.core.services import RcrainfoService, get_rcrainfo_client +from apps.core.services import RcraClient, get_rcra_client from apps.handler.models import QuickerSign from apps.handler.serializers import QuickerSignSerializer from apps.manifest.models import Manifest @@ -65,9 +65,9 @@ class PullManifestsResult(TypedDict): class EManifest: """IO interface with the e-Manifest system.""" - def __init__(self, *, username: Optional[str], rcrainfo: Optional[RcrainfoService] = None): + def __init__(self, *, username: Optional[str], rcrainfo: Optional[RcraClient] = None): self.username = username - self.rcrainfo = rcrainfo or get_rcrainfo_client(username=username) + self.rcrainfo = rcrainfo or get_rcra_client(username=username) @property def is_available(self) -> bool: @@ -185,7 +185,7 @@ def get_updated_mtn(site_id: str, last_sync_date: datetime, rcra_client) -> list @transaction.atomic def sync_manifests( - *, site_id: str, last_sync_date: datetime, rcra_client: RcrainfoService + *, site_id: str, last_sync_date: datetime, rcra_client: RcraClient ) -> PullManifestsResult: """Pull manifests and update the last sync date for a site""" updated_mtn = get_updated_mtn( diff --git a/server/apps/manifest/services/emanifest_search.py b/server/apps/manifest/services/emanifest_search.py index e3cedf3d..66d0806b 100644 --- a/server/apps/manifest/services/emanifest_search.py +++ b/server/apps/manifest/services/emanifest_search.py @@ -1,7 +1,7 @@ from datetime import UTC, datetime, timedelta, timezone from typing import Literal, Optional, get_args -from apps.core.services import RcrainfoService +from apps.core.services import RcraClient EmanifestStatus = Literal[ "Pending", @@ -24,8 +24,8 @@ class EmanifestSearch: - def __init__(self, rcra_client: Optional[RcrainfoService] = None): - self._rcra_client = rcra_client or RcrainfoService() + def __init__(self, rcra_client: Optional[RcraClient] = None): + self._rcra_client = rcra_client or RcraClient() self.state_code: Optional[str] = None self.site_id: Optional[str] = None self.status: Optional[EmanifestStatus] = None @@ -38,7 +38,7 @@ def __init__(self, rcra_client: Optional[RcrainfoService] = None): @property def rcra_client(self): if not self._rcra_client: - self._rcra_client = RcrainfoService() + self._rcra_client = RcraClient() return self._rcra_client @rcra_client.setter diff --git a/server/apps/manifest/tasks.py b/server/apps/manifest/tasks.py index a39054d0..d41b684f 100644 --- a/server/apps/manifest/tasks.py +++ b/server/apps/manifest/tasks.py @@ -4,7 +4,7 @@ from celery import Task, shared_task, states from celery.exceptions import Ignore, Reject -from apps.core.services import get_rcrainfo_client +from apps.core.services import get_rcra_client logger = logging.getLogger(__name__) @@ -64,7 +64,7 @@ def sync_site_manifests(self, *, site_id: str, username: str): from apps.site.services import get_user_site, update_emanifest_sync_date try: - client = get_rcrainfo_client(username=username) + client = get_rcra_client(username=username) site = get_user_site(username=username, epa_id=site_id) results = sync_manifests( site_id=site_id, last_sync_date=site.last_rcrainfo_manifest_sync, rcra_client=client diff --git a/server/apps/manifest/tests/conftest.py b/server/apps/manifest/tests/conftest.py index 2c21e259..cb1015ee 100644 --- a/server/apps/manifest/tests/conftest.py +++ b/server/apps/manifest/tests/conftest.py @@ -54,7 +54,20 @@ def create_manifest( ), ) except IntegrityError: - mtn = None + return Manifest.objects.create( + mtn=fake.mtn(), + status=status or fake.status(), + created_date=datetime.now(UTC), + potential_ship_date=datetime.now(UTC), + generator=generator + or manifest_handler_factory( + rcra_site=rcra_site_factory(site_type=RcraSiteType.GENERATOR) + ), + tsdf=tsdf + or manifest_handler_factory( + rcra_site=rcra_site_factory(site_type=RcraSiteType.TSDF) + ), + ) return create_manifest diff --git a/server/apps/manifest/tests/test_emanifest_service.py b/server/apps/manifest/tests/test_emanifest_service.py index 130a2749..0323635a 100644 --- a/server/apps/manifest/tests/test_emanifest_service.py +++ b/server/apps/manifest/tests/test_emanifest_service.py @@ -2,7 +2,7 @@ import pytest_mock from rest_framework import status -from apps.core.services import RcrainfoService, get_rcrainfo_client +from apps.core.services import RcraClient, get_rcra_client from apps.manifest.services import EManifest @@ -16,7 +16,7 @@ def _setup(self, user_factory, site_factory, haztrak_json): @pytest.fixture def manifest_100033134elc_rcra_response(self, haztrak_json, mock_responses): - rcrainfo = get_rcrainfo_client(api_id="my_mock_id", api_key="my_mock_key") + rcrainfo = get_rcra_client(api_id="my_mock_id", api_key="my_mock_key") manifest_json = haztrak_json.MANIFEST.value mock_responses.get( url=f'{rcrainfo.base_url}v1/emanifest/manifest/{manifest_json.get("manifestTrackingNumber")}', @@ -29,7 +29,7 @@ def test_pull_manifests( self, manifest_100033134elc_rcra_response, mocker: pytest_mock.MockerFixture ): """Test retrieves a manifest from RCRAInfo""" - rcrainfo = RcrainfoService(auto_renew=False) + rcrainfo = RcraClient(auto_renew=False) emanifest = EManifest(username=self.user.username, rcrainfo=rcrainfo) results = emanifest.pull(tracking_numbers=[self.tracking_number]) assert self.tracking_number in results["success"] diff --git a/server/apps/manifest/tests/test_search_emanifest.py b/server/apps/manifest/tests/test_search_emanifest.py index a02d2924..c30591c1 100644 --- a/server/apps/manifest/tests/test_search_emanifest.py +++ b/server/apps/manifest/tests/test_search_emanifest.py @@ -3,7 +3,7 @@ import pytest -from apps.core.services import RcrainfoService +from apps.core.services import RcraClient from apps.manifest.services.emanifest_search import EmanifestSearch @@ -17,7 +17,7 @@ def test_defaults_to_unauthenticated_rcra_client(self): def test_execute_sends_a_request_to_rcrainfo( self, mock_responses, mock_emanifest_auth_response ): - stub_rcra_client = RcrainfoService(rcrainfo_env="preprod", api_key="foo", api_id="foo") + stub_rcra_client = RcraClient(rcrainfo_env="preprod", api_key="foo", api_id="foo") mock_responses.post( "https://rcrainfopreprod.epa.gov/rcrainfo/rest/api/v1/emanifest/search" ) @@ -26,7 +26,7 @@ def test_execute_sends_a_request_to_rcrainfo( @pytest.mark.parametrize("mock_emanifest_auth_response", [["foo", "foo"]], indirect=True) def test_search_builds_json(self, mock_responses, mock_emanifest_auth_response): - stub_rcra_client = RcrainfoService(rcrainfo_env="preprod", api_key="foo", api_id="foo") + stub_rcra_client = RcraClient(rcrainfo_env="preprod", api_key="foo", api_id="foo") mock_responses.post( "https://rcrainfopreprod.epa.gov/rcrainfo/rest/api/v1/emanifest/search" ) @@ -103,7 +103,7 @@ def test_add_end_date(self): def test_add_end_date_defaults_to_now(self): now = datetime.now(UTC) search = EmanifestSearch().add_end_date() - end_date = datetime.strptime(search.end_date, RcrainfoService.datetime_format) + end_date = datetime.strptime(search.end_date, RcraClient.datetime_format) assert end_date.day == now.day assert end_date.month == now.month assert end_date.year == now.year diff --git a/server/apps/profile/services.py b/server/apps/profile/services.py index d44312bf..34e39d98 100644 --- a/server/apps/profile/services.py +++ b/server/apps/profile/services.py @@ -6,7 +6,7 @@ from django.db import transaction from apps.core.models import TrakUser -from apps.core.services import RcrainfoService, get_rcrainfo_client +from apps.core.services import RcraClient, get_rcra_client from apps.profile.models import Profile, RcrainfoProfile, RcrainfoSiteAccess from apps.profile.serializers import RcrainfoSitePermissionsSerializer from apps.rcrasite.models import RcraSite @@ -61,11 +61,11 @@ class RcraProfileService: of a and exposes method corresponding to use cases. """ - def __init__(self, *, username: str, rcrainfo: Optional[RcrainfoService] = None): + def __init__(self, *, username: str, rcrainfo: Optional[RcraClient] = None): self.username = username profile, created = get_or_create_rcra_profile(username=username) self.profile: RcrainfoProfile = profile - self.rcrainfo = rcrainfo or get_rcrainfo_client(username=username) + self.rcrainfo = rcrainfo or get_rcra_client(username=username) def update_rcrainfo_profile(self, *, rcrainfo_username: Optional[str] = None) -> None: """ diff --git a/server/apps/rcrasite/services/__init__.py b/server/apps/rcrasite/services/__init__.py new file mode 100644 index 00000000..064c74d6 --- /dev/null +++ b/server/apps/rcrasite/services/__init__.py @@ -0,0 +1 @@ +from .rcra_site import RcraSiteService, query_rcra_sites diff --git a/server/apps/rcrasite/services.py b/server/apps/rcrasite/services/rcra_site.py similarity index 91% rename from server/apps/rcrasite/services.py rename to server/apps/rcrasite/services/rcra_site.py index 6224e958..3e2202e8 100644 --- a/server/apps/rcrasite/services.py +++ b/server/apps/rcrasite/services/rcra_site.py @@ -6,7 +6,7 @@ from django.db.models import QuerySet from rest_framework.exceptions import ValidationError -from apps.core.services import RcrainfoService, get_rcrainfo_client +from apps.core.services import RcraClient, get_rcra_client from apps.rcrasite.models import RcraSite from apps.rcrasite.serializers import RcraSiteSerializer @@ -41,15 +41,9 @@ class RcraSiteService: directly relate to use cases. """ - def __init__(self, *, username: str, rcrainfo: Optional[RcrainfoService] = None): + def __init__(self, *, username: str, rcrainfo: Optional[RcraClient] = None): self.username = username - self.rcrainfo = rcrainfo or get_rcrainfo_client(username=self.username) - - def __repr__(self): - return ( - f"<{self.__class__.__name__}(api_username='{self.username}', " - f"rcrainfo='{self.rcrainfo}')>" - ) + self.rcrainfo = rcrainfo or get_rcra_client(username=self.username) def pull_rcrainfo_site(self, *, site_id: str) -> RcraSite: """Retrieve a site/rcra_site from Rcrainfo and return RcraSiteSerializer""" diff --git a/server/apps/wasteline/tasks.py b/server/apps/wasteline/tasks.py index 2f8cb583..b52df7c2 100644 --- a/server/apps/wasteline/tasks.py +++ b/server/apps/wasteline/tasks.py @@ -9,11 +9,11 @@ @shared_task(name="pull_federal_code", bind=True) def pull_federal_codes(self, api_user: Optional[str] = None): - from apps.core.services import get_rcrainfo_client + from apps.core.services import get_rcra_client logger.debug(f"start task {self.name}") try: - rcrainfo = get_rcrainfo_client(username=api_user) + rcrainfo = get_rcra_client(username=api_user) return rcrainfo.sync_federal_waste_codes() except (ConnectionError, TimeoutError): raise Reject() From 27eb34b8de9aeef3b5848d5ae0e29e67ac52335b Mon Sep 17 00:00:00 2001 From: David Graham Date: Tue, 4 Jun 2024 20:27:48 -0400 Subject: [PATCH 4/7] RcraSiteSearch class --- server/apps/rcrasite/services/__init__.py | 1 + .../rcrasite/services/rcra_site_search.py | 42 +++++++++++++++++ .../apps/rcrasite/tests/test_site_search.py | 46 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 server/apps/rcrasite/services/rcra_site_search.py create mode 100644 server/apps/rcrasite/tests/test_site_search.py diff --git a/server/apps/rcrasite/services/__init__.py b/server/apps/rcrasite/services/__init__.py index 064c74d6..a174ed69 100644 --- a/server/apps/rcrasite/services/__init__.py +++ b/server/apps/rcrasite/services/__init__.py @@ -1 +1,2 @@ from .rcra_site import RcraSiteService, query_rcra_sites +from .rcra_site_search import RcraSiteSearch diff --git a/server/apps/rcrasite/services/rcra_site_search.py b/server/apps/rcrasite/services/rcra_site_search.py new file mode 100644 index 00000000..53be1bd9 --- /dev/null +++ b/server/apps/rcrasite/services/rcra_site_search.py @@ -0,0 +1,42 @@ +from typing import Optional + +from apps.core.services import RcraClient + + +class RcraSiteSearch: + def __init__(self, rcra_client: Optional[RcraClient] = None): + self._rcra_client = rcra_client or RcraClient() + self._state: Optional[str] = None + self._epa_id: Optional[str] = None + + @property + def rcra_client(self) -> RcraClient: + if not self._rcra_client: + self._rcra_client = RcraClient() + return self._rcra_client + + @rcra_client.setter + def rcra_client(self, value) -> None: + self._rcra_client = value + + def build_search_args(self) -> dict: + search_params = { + "state": self._state, + "epaSiteId": self._epa_id, + } + return {k: v for k, v in search_params.items() if v is not None} + + def state(self, state: str) -> "RcraSiteSearch": + self._state = state + return self + + def epa_id(self, epa_id: str) -> "RcraSiteSearch": + self._epa_id = epa_id + return self + + def outputs(self) -> dict: + return self.build_search_args() + + def execute(self): + search_args = self.build_search_args() + return self._rcra_client.search_sites(**search_args) diff --git a/server/apps/rcrasite/tests/test_site_search.py b/server/apps/rcrasite/tests/test_site_search.py new file mode 100644 index 00000000..b56d5ec2 --- /dev/null +++ b/server/apps/rcrasite/tests/test_site_search.py @@ -0,0 +1,46 @@ +import json + +import pytest + +from apps.core.services import RcraClient +from apps.rcrasite.services import RcraSiteSearch + + +class TestRcraSiteSearchClass: + def test_defaults_to_unauthenticated_rcra_client(self): + search = RcraSiteSearch() + assert search.rcra_client is not None + assert search.rcra_client.is_authenticated is False + + @pytest.mark.parametrize("mock_emanifest_auth_response", [["foo", "foo"]], indirect=True) + def test_execute_sends_a_request_to_rcrainfo( + self, mock_responses, mock_emanifest_auth_response + ): + stub_rcra_client = RcraClient(rcrainfo_env="preprod", api_key="foo", api_id="foo") + mock_responses.post("https://rcrainfopreprod.epa.gov/rcrainfo/rest/api/v1/site-search") + result = RcraSiteSearch(stub_rcra_client).execute() + assert result.status_code == 200 + + @pytest.mark.parametrize("mock_emanifest_auth_response", [["foo", "foo"]], indirect=True) + def test_search_builds_json(self, mock_responses, mock_emanifest_auth_response): + stub_rcra_client = RcraClient(rcrainfo_env="preprod", api_key="foo", api_id="foo") + mock_responses.post("https://rcrainfopreprod.epa.gov/rcrainfo/rest/api/v1/site-search") + RcraSiteSearch(stub_rcra_client).state("VA").epa_id("VATESTGEN001").execute() + for call in mock_responses.calls: + if "site-search" in call.request.url: + request_body = json.loads(call.request.body) + assert request_body["state"] == "VA" + assert request_body["epaSiteId"] == "VATESTGEN001" + + class TestBuildSearchWithState: + def test_add_state_code(self): + search = RcraSiteSearch().state("CA") + assert "CA" in search.outputs().values() + assert "state" in search.outputs().keys() + + class TestBuildSearchWithEpaId: + def test_add_state_code(self): + partial_id = "VATEST" + search = RcraSiteSearch().epa_id(partial_id) + assert partial_id in search.outputs().values() + assert "epaSiteId" in search.outputs().keys() From 8e6056a837262e5763b349de7913978b3c72bb9a Mon Sep 17 00:00:00 2001 From: David Graham Date: Tue, 4 Jun 2024 21:12:54 -0400 Subject: [PATCH 5/7] RcraSiteSearch validates site searhc parameters --- .../rcrasite/services/rcra_site_search.py | 27 +++++++++++++++++++ .../apps/rcrasite/tests/test_site_search.py | 8 ++++++ 2 files changed, 35 insertions(+) diff --git a/server/apps/rcrasite/services/rcra_site_search.py b/server/apps/rcrasite/services/rcra_site_search.py index 53be1bd9..7ac5a65d 100644 --- a/server/apps/rcrasite/services/rcra_site_search.py +++ b/server/apps/rcrasite/services/rcra_site_search.py @@ -19,6 +19,13 @@ def rcra_client(self) -> RcraClient: def rcra_client(self, value) -> None: self._rcra_client = value + def get_search_attributes(self) -> dict: + search_params = { + "state": self._state, + "epa_id": self._epa_id, + } + return {k: v for k, v in search_params.items() if v is not None} + def build_search_args(self) -> dict: search_params = { "state": self._state, @@ -26,6 +33,18 @@ def build_search_args(self) -> dict: } return {k: v for k, v in search_params.items() if v is not None} + def _validate_state(self, state: str) -> None: + if not state.isalpha(): + raise ValueError("State code must be alphabetical") + if len(state) != 2: + raise ValueError("State code must be two letters") + + def _validate_epa_id(self, epa_id: str) -> None: + if len(epa_id) <= 2: + raise ValueError("EPA ID must be at least 2 characters") + if len(epa_id) != 12 and self._state is None: + raise ValueError("EPA ID must be 12 characters if state is not provided") + def state(self, state: str) -> "RcraSiteSearch": self._state = state return self @@ -37,6 +56,14 @@ def epa_id(self, epa_id: str) -> "RcraSiteSearch": def outputs(self) -> dict: return self.build_search_args() + def validate(self): + search_args = self.get_search_attributes() + for key, value in search_args.items(): + validate_method = getattr(self, f"_validate_{key}") + validate_method(value) + return True + def execute(self): + self.validate() search_args = self.build_search_args() return self._rcra_client.search_sites(**search_args) diff --git a/server/apps/rcrasite/tests/test_site_search.py b/server/apps/rcrasite/tests/test_site_search.py index b56d5ec2..a07f4909 100644 --- a/server/apps/rcrasite/tests/test_site_search.py +++ b/server/apps/rcrasite/tests/test_site_search.py @@ -44,3 +44,11 @@ def test_add_state_code(self): search = RcraSiteSearch().epa_id(partial_id) assert partial_id in search.outputs().values() assert "epaSiteId" in search.outputs().keys() + + class TestValidation: + def test_no_validation_error_raised(self): + assert RcraSiteSearch().state("CA").epa_id("foo").validate() + + def test_error_when_partial_id_without_other_params(self): + with pytest.raises(ValueError): + RcraSiteSearch().epa_id("foo").validate() From bd58dfe7784cbdbef12032602d627a924200e32a Mon Sep 17 00:00:00 2001 From: David Graham Date: Tue, 4 Jun 2024 21:22:14 -0400 Subject: [PATCH 6/7] add debugging methods for RcraSiteSearch class and implement --- server/apps/rcrasite/services/rcra_site.py | 9 ++++++++- server/apps/rcrasite/services/rcra_site_search.py | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/server/apps/rcrasite/services/rcra_site.py b/server/apps/rcrasite/services/rcra_site.py index 3e2202e8..e14b3f51 100644 --- a/server/apps/rcrasite/services/rcra_site.py +++ b/server/apps/rcrasite/services/rcra_site.py @@ -10,6 +10,8 @@ from apps.rcrasite.models import RcraSite from apps.rcrasite.serializers import RcraSiteSerializer +from .rcra_site_search import RcraSiteSearch + class HandlerSearchResults(TypedDict): sites: list[RcraSite] @@ -72,7 +74,12 @@ def search_rcrainfo_handlers(self, **search_parameters) -> HandlerSearchResults: try: data = cache.get(cache_key) if not data: - response = self.rcrainfo.search_sites(**search_parameters) + response = ( + RcraSiteSearch(rcra_client=self.rcrainfo) + .state(search_parameters.get("state")) + .epa_id(search_parameters.get("epaSiteId")) + .execute() + ) if response.ok: data = response.json() cache.set(cache_key, data, 60 * 60 * 24) diff --git a/server/apps/rcrasite/services/rcra_site_search.py b/server/apps/rcrasite/services/rcra_site_search.py index 7ac5a65d..812f3aff 100644 --- a/server/apps/rcrasite/services/rcra_site_search.py +++ b/server/apps/rcrasite/services/rcra_site_search.py @@ -1,5 +1,7 @@ from typing import Optional +from emanifest import RcrainfoResponse + from apps.core.services import RcraClient @@ -63,7 +65,7 @@ def validate(self): validate_method(value) return True - def execute(self): + def execute(self) -> RcrainfoResponse: self.validate() search_args = self.build_search_args() return self._rcra_client.search_sites(**search_args) From 069d1e3b72e8cc293bf852c2603b907e9d5f9f1c Mon Sep 17 00:00:00 2001 From: David Graham Date: Tue, 4 Jun 2024 21:46:15 -0400 Subject: [PATCH 7/7] add site_type search parameter to RcraSiteSearch class --- server/apps/rcrasite/services/rcra_site.py | 2 +- .../rcrasite/services/rcra_site_search.py | 19 ++++++++++++++++--- .../apps/rcrasite/tests/test_site_search.py | 7 +++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/server/apps/rcrasite/services/rcra_site.py b/server/apps/rcrasite/services/rcra_site.py index e14b3f51..d418daef 100644 --- a/server/apps/rcrasite/services/rcra_site.py +++ b/server/apps/rcrasite/services/rcra_site.py @@ -76,7 +76,7 @@ def search_rcrainfo_handlers(self, **search_parameters) -> HandlerSearchResults: if not data: response = ( RcraSiteSearch(rcra_client=self.rcrainfo) - .state(search_parameters.get("state")) + .site_type(search_parameters.get("siteType")) .epa_id(search_parameters.get("epaSiteId")) .execute() ) diff --git a/server/apps/rcrasite/services/rcra_site_search.py b/server/apps/rcrasite/services/rcra_site_search.py index 812f3aff..a993c1a2 100644 --- a/server/apps/rcrasite/services/rcra_site_search.py +++ b/server/apps/rcrasite/services/rcra_site_search.py @@ -1,15 +1,18 @@ -from typing import Optional +from typing import Literal, Optional from emanifest import RcrainfoResponse from apps.core.services import RcraClient +SiteType = Literal["Generator", "Tsdf", "Transporter", "Broker"] + class RcraSiteSearch: def __init__(self, rcra_client: Optional[RcraClient] = None): self._rcra_client = rcra_client or RcraClient() self._state: Optional[str] = None self._epa_id: Optional[str] = None + self._site_type: Optional[str] = None @property def rcra_client(self) -> RcraClient: @@ -25,6 +28,7 @@ def get_search_attributes(self) -> dict: search_params = { "state": self._state, "epa_id": self._epa_id, + "site_type": self._site_type, } return {k: v for k, v in search_params.items() if v is not None} @@ -32,6 +36,7 @@ def build_search_args(self) -> dict: search_params = { "state": self._state, "epaSiteId": self._epa_id, + "siteType": self._site_type, } return {k: v for k, v in search_params.items() if v is not None} @@ -44,8 +49,16 @@ def _validate_state(self, state: str) -> None: def _validate_epa_id(self, epa_id: str) -> None: if len(epa_id) <= 2: raise ValueError("EPA ID must be at least 2 characters") - if len(epa_id) != 12 and self._state is None: - raise ValueError("EPA ID must be 12 characters if state is not provided") + if len(epa_id) != 12 and (self._site_type is None and self._state is None): + raise ValueError("EPA ID must be 12 characters") + + def _validate_site_type(self, site_type: SiteType) -> None: + if site_type not in ["Generator", "Tsdf", "Transporter", "Broker"]: + raise ValueError("Invalid site type") + + def site_type(self, site_type: str) -> "RcraSiteSearch": + self._site_type = site_type + return self def state(self, state: str) -> "RcraSiteSearch": self._state = state diff --git a/server/apps/rcrasite/tests/test_site_search.py b/server/apps/rcrasite/tests/test_site_search.py index a07f4909..16b46df9 100644 --- a/server/apps/rcrasite/tests/test_site_search.py +++ b/server/apps/rcrasite/tests/test_site_search.py @@ -45,6 +45,13 @@ def test_add_state_code(self): assert partial_id in search.outputs().values() assert "epaSiteId" in search.outputs().keys() + class TestBuildSearchWithSiteType: + def test_add_state_code(self): + site_type = "Generator" + search = RcraSiteSearch().site_type(site_type) + assert site_type in search.outputs().values() + assert "siteType" in search.outputs().keys() + class TestValidation: def test_no_validation_error_raised(self): assert RcraSiteSearch().state("CA").epa_id("foo").validate()