feat: external exposure-provider integration, GXM adapter phase 1 - #1386
Draft
benhayes21 wants to merge 1 commit into
Draft
feat: external exposure-provider integration, GXM adapter phase 1#1386benhayes21 wants to merge 1 commit into
benhayes21 wants to merge 1 commit into
Conversation
Adds a provider-agnostic external exposure-data layer to the Platform 2
API, with GXM as the first concrete provider. Two use cases are
implemented:
- UC1 (full exposure retrieval): POST providers/{p}/portfolios/{id}/external_location_file/
fetches a country-level (or bbox-filtered) OED location file from the
named provider and attaches it to the portfolio.
- UC2 (attribute enrichment): POST providers/{p}/portfolios/{id}/external_enrich/
fills blank OED fields (OccupancyCode, ConstructionCode, etc.) from a
per-point GXM lookup without overwriting user-supplied values.
Both endpoints are async (202 + job_id); progress is polled via
GET providers/{p}/portfolios/{id}/external_jobs/{job_id}/.
An admin-only GET/PUT external_provider_settings/{provider}/ endpoint
stores credentials.
All new endpoints are gated behind EXTERNAL_PROVIDERS_ENABLED (default
false), so this can be merged to main without activating the feature.
New app: src/server/oasisapi/external_providers/
adapters/base.py – abstract ExposureProvider interface
adapters/gxm.py – GXM OAuth2 client + streaming HTTP
models.py – ExternalJob (UUID PK) + ExternalProviderSettings
services.py – orchestration, non-destructive merge, settings fallback
tasks.py – Celery task wrappers (oasis-internal-worker queue)
views.py / urls.py – DRF views with drf-spectacular annotations
migrations/0001 – creates both models
tests/ – unit tests (merge logic, service flows, view layer)
Modified:
portfolios/models.py – location_file_source / _external_provider / _audit
portfolios/v2_api/serializers – location_file dict gains source/external_provider/audit_url
portfolios/migrations/0011 – adds three new portfolio fields
settings/base.py – EXTERNAL_PROVIDERS_ENABLED + GXM_* settings
swagger.py – registers v2-external-providers URL namespace
CLAUDE.md – project documentation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
benhayes21
marked this pull request as draft
April 22, 2026 10:23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1385
Summary
src/server/oasisapi/external_providers/implementing a provider-agnostic external exposure-data layerEXTERNAL_PROVIDERS_ENABLED(defaultfalse) — zero change to existing behaviour when offWhat changed
New app (
external_providers/)adapters/base.py— abstractExposureProviderinterfaceadapters/gxm.py— GXM OAuth2 client-credentials + chunked HTTP streaming (CSV + Parquet)models.py—ExternalJob(UUID PK, tracks status/task_id/result/audit) +ExternalProviderSettings(admin-editable creds)services.py— orchestration, non-destructive merge for UC2,iniconf-compatible settings fallbacktasks.py— Celery task wrappers dispatched tooasis-internal-workerviews.py/urls.py— DRF views withdrf-spectacularannotations;{provider}is a URL segment, not a slugmigrations/0001_initial.pytests/— unit tests for merge logic, service flows, view layer (flag-off/202/validation/admin); GXM adapter integration tests auto-skip withoutOASIS_GXM_BASE_URLModified
portfolios/models.py— addslocation_file_source/location_file_external_provider/location_file_auditportfolios/v2_api/serializers.py—location_filedict gainssource,external_provider,audit_url(additive)portfolios/migrations/0011— new portfolio fieldssettings/base.py—EXTERNAL_PROVIDERS_ENABLED+GXM_BASE_URL/CLIENT_ID/CLIENT_SECRETswagger.py— registersv2-external-providersURL namespaceTest plan
EXTERNAL_PROVIDERS_ENABLED=false(default):pytest src/server/oasisapi/— no regressions, all existing tests passGET /portfolios/{id}/with feature off:location_file.source == "user_upload",audit_url == null(backward-compat regression test intest_views.py)pytest src/server/oasisapi/external_providers/tests/test_services.py -k "not django_db"— pure merge/utility logic, no DB neededOASIS_GXM_BASE_URL=http://localhost:8000): full UC1 and UC2 end-to-end viacurl(seeexternal_providers/README.md)Known gaps (tracked in #1385)
location_file_sourcewhen user manually re-uploads via existing endpoint🤖 Generated with Claude Code