Skip to content

Commit 9d23fee

Browse files
authored
Merge pull request #227 from helpfulengineering/feat/okw-network-map-endpoint
feat(okw): network map endpoint — local facilities ∪ Maps of Making
2 parents d513c20 + 106d1e9 commit 9d23fee

8 files changed

Lines changed: 727 additions & 2 deletions

File tree

.repo-map.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ REPOSITORY MAP (Aider Style)
33
================================================================================
44
Repository: supply-graph-ai
55

6-
Total Python files: 436
6+
Total Python files: 438
77

88
├── deploy/
99
│ ├── __init__.py
@@ -1272,6 +1272,7 @@ Total Python files: 436
12721272
│ │ ├── matching_service.py
12731273
│ │ │ class MatchingService (__init__, _normalize_req_cap_lists, _normalize_capability_list...)
12741274
│ │ ├── mom_bridge.py
1275+
│ │ │ class MoMSpacesCache (__init__, is_fresh, invalidate)
12751276
│ │ ├── okh_service.py
12761277
│ │ │ class OKHService (__init__)
12771278
│ │ ├── okw_service.py
@@ -1679,6 +1680,9 @@ Total Python files: 436
16791680
│ │ def _classify()
16801681
│ │ def test_matching_ab_report_remote_okw()
16811682
│ ├── test_matching_baseline.py
1683+
│ ├── test_okw_map_route.py
1684+
│ │ def _facility_content()
1685+
│ │ def test_map_local_only_returns_source_labeled_points()
16821686
│ ├── test_reverse_facility_match_route.py
16831687
│ │ def _facility_content()
16841688
│ │ def test_reverse_match_unknown_facility_returns_404()
@@ -1944,6 +1948,11 @@ Total Python files: 436
19441948
│ def _request()
19451949
│ def test_parse_match_filters_populates_okw_ids()
19461950
│ def test_parse_match_filters_omits_okw_ids_when_empty()
1951+
├── test_okw_map.py
1952+
│ class _FakeResponse (__init__, raise_for_status, json)
1953+
│ class _FakeClient (__init__)
1954+
│ def _bindings()
1955+
│ def _facility()
19471956
├── test_package_pin.py
19481957
│ def _sha256()
19491958
│ def _build_fake_package()
@@ -2041,7 +2050,7 @@ Total Python files: 436
20412050

20422051
## Overview
20432052

2044-
Total files analyzed: 436
2053+
Total files analyzed: 438
20452054

20462055
## Entry Points
20472056

@@ -2341,6 +2350,15 @@ Orchestrates direc...
23412350
### `src/core/services/mom_bridge.py`
23422351
> MoM SPARQL bridge — query Maps of Making for spaces matching an OHM process....
23432352
2353+
**Exports:** MoMSpacesCache
2354+
2355+
**Classes:**
2356+
- `MoMSpacesCache`
2357+
- Methods: is_fresh, invalidate
2358+
- TTL cache for the MoM all-spaces map layer.
2359+
2360+
Serves the last successful fetch fo...
2361+
23442362
### `src/core/services/okh_service.py`
23452363

23462364
**Exports:** OKHService
@@ -7608,6 +7626,16 @@ Uses a minima...
76087626

76097627
**Internal Dependencies:** 4 imports
76107628

7629+
### `tests/integration/test_okw_map_route.py`
7630+
> Integration test for GET /api/okw/map (network map, review #1).
7631+
7632+
Runs the full ASGI app in-process. ...
7633+
7634+
**Exports:** test_map_local_only_returns_source_labeled_points
7635+
7636+
**Functions:**
7637+
- `test_map_local_only_returns_source_labeled_points(client)`
7638+
76117639
### `tests/integration/test_reverse_facility_match_route.py`
76127640
> Integration tests for POST /api/match/facility (reverse matching, review #7).
76137641
@@ -8048,6 +8076,18 @@ A caller can pre-select which...
80488076

80498077
**Internal Dependencies:** 11 imports
80508078

8079+
### `tests/unit/test_okw_map.py`
8080+
> Unit tests for the OKW network map: MoM all-spaces fetch, TTL cache, union.
8081+
8082+
Covers `fetch_all_mom_s...
8083+
8084+
**Classes:**
8085+
- `_FakeResponse`
8086+
- Methods: raise_for_status, json
8087+
- `_FakeClient`
8088+
8089+
**Internal Dependencies:** 13 imports
8090+
80518091
### `tests/unit/test_package_pin.py`
80528092
> Unit tests for OKH package pin record creation and verification (issue #174)....
80538093

docs/models/okw-docs.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,25 @@ if facility.has_process("3d printing"):
581581
The check searches both `manufacturing_processes` URLs and equipment capability
582582
descriptions.
583583

584+
## Network Map & Maps of Making
585+
586+
`GET /api/okw/map` (and `ohm okw map`) return the point set for the web UI's
587+
network map: **local OKW facilities unioned with [Maps of Making](https://mapsofmaking.org)
588+
(MoM) spaces**. Each point is source-labeled — `{id, name, lat, lon, source}` with
589+
`source` in `{"local", "mom"}` — so the map layer is source-agnostic and can grow
590+
to further networks.
591+
592+
- **Coordinates** come from `Location.coordinates()` (the decimal-degrees
593+
accessor). Local facilities without parseable coordinates are counted in
594+
`dropped_no_coords` rather than plotted.
595+
- **MoM** is fetched from a **24h TTL cache** (`MoMSpacesCache` in
596+
`services/mom_bridge.py`) built on the existing SPARQL bridge. It **degrades
597+
gracefully**: if MoM is unreachable the response is local-only with
598+
`mom_available: false`, and a stale cache keeps serving until a refresh
599+
succeeds. `mom_spaces_cache.refresh()` / `.invalidate()` are the cache-refresh
600+
hooks other events can call; the endpoint's `force_refresh=true` (and CLI
601+
`--refresh`) trigger a refresh on demand.
602+
584603
## CLI Commands
585604

586605
| Command | Description |
@@ -593,13 +612,15 @@ descriptions.
593612
| `ohm okw export` | Export the OKW JSON Schema |
594613
| `ohm okw template` | Output a blank facility template |
595614
| `ohm okw create-interactive` | Interactively build a new facility |
615+
| `ohm okw map` | Network map points: local facilities ∪ Maps of Making (`--no-mom`, `--refresh`) |
596616

597617
## API Endpoints
598618

599619
| Method | Path | Description |
600620
|---|---|---|
601621
| `GET` | `/api/okw/export` | Export OKW JSON Schema |
602622
| `GET` | `/api/okw/template` | Get blank facility template |
623+
| `GET` | `/api/okw/map` | Network map points (local OKW ∪ Maps of Making); `include_mom`, `force_refresh` |
603624
| `POST` | `/api/okw/create` | Create facility from JSON body |
604625
| `POST` | `/api/okw/validate` | Validate a facility dict |
605626
| `POST` | `/api/okw/upload` | Upload a facility file |

src/cli/okw.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,99 @@ async def fallback_list():
498498
raise
499499

500500

501+
@okw_group.command(name="map")
502+
@click.option(
503+
"--no-mom",
504+
is_flag=True,
505+
help="Exclude Maps of Making; show local OKW facilities only.",
506+
)
507+
@click.option(
508+
"--refresh",
509+
is_flag=True,
510+
help="Force a refresh of the MoM cache before building the map.",
511+
)
512+
@standard_cli_command(
513+
help_text="""
514+
List network map points: local OKW facilities unioned with Maps of Making.
515+
516+
Each point is source-labeled ("local" or "mom"). Local facilities without
517+
coordinates are counted but not plotted. MoM is served from a 24h cache and
518+
degrades gracefully — if it is unreachable you get local-only points.
519+
""",
520+
epilog="""
521+
Examples:
522+
ohm okw map # local + MoM
523+
ohm okw map --no-mom # local facilities only
524+
ohm okw map --refresh # force a MoM cache refresh first
525+
""",
526+
async_cmd=True,
527+
track_performance=True,
528+
handle_errors=True,
529+
format_output=True,
530+
add_llm_config=False,
531+
)
532+
@click.pass_context
533+
async def okw_map(
534+
ctx,
535+
no_mom: bool,
536+
refresh: bool,
537+
verbose: bool,
538+
output_format: str,
539+
use_llm: bool,
540+
llm_provider: str,
541+
llm_model: Optional[str],
542+
quality_level: str,
543+
strict_mode: bool,
544+
):
545+
"""List network map points (local OKW facilities ∪ Maps of Making)."""
546+
cli_ctx = ctx.obj
547+
cli_ctx.start_command_tracking("okw-map")
548+
include_mom = not no_mom
549+
550+
try:
551+
552+
async def http_map():
553+
cli_ctx.log("Fetching map points via HTTP API...", "info")
554+
params = {
555+
"include_mom": str(include_mom).lower(),
556+
"force_refresh": str(refresh).lower(),
557+
}
558+
return await cli_ctx.api_client.request(
559+
"GET", "/api/okw/map", params=params
560+
)
561+
562+
async def fallback_map():
563+
cli_ctx.log("Building map points via direct service...", "info")
564+
okw_service = await OKWService.get_instance()
565+
return await okw_service.get_map_points(
566+
include_mom=include_mom, force_refresh=refresh
567+
)
568+
569+
command = SmartCommand(cli_ctx)
570+
result = await command.execute_with_fallback(http_map, fallback_map)
571+
data = result.get("data", result) if isinstance(result, dict) else result
572+
573+
if output_format == "json":
574+
click.echo(json.dumps(data, indent=2))
575+
else:
576+
mom_state = "available" if data.get("mom_available") else "unavailable"
577+
click.echo(
578+
f"🗺️ {len(data.get('points', []))} map points "
579+
f"({data.get('local_count', 0)} local + {data.get('mom_count', 0)} MoM)"
580+
)
581+
dropped = data.get("dropped_no_coords", 0)
582+
if dropped:
583+
click.echo(f" {dropped} local facility(ies) omitted (no coordinates)")
584+
if include_mom:
585+
click.echo(f" Maps of Making: {mom_state}")
586+
587+
cli_ctx.end_command_tracking()
588+
589+
except Exception as e:
590+
cli_ctx.log(f"Map build failed: {str(e)}", "error")
591+
raise
592+
593+
501594
@okw_group.command()
502595
@click.argument("facility_id", type=str)
503596
@click.option("--force", is_flag=True, help="Force deletion without confirmation")

src/core/api/routes/okw.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,49 @@ async def get_okw_template(http_request: Request = None) -> Any:
393393
}
394394

395395

396+
@router.get(
397+
"/map",
398+
summary="Network map points (local OKW ∪ Maps of Making)",
399+
description="""
400+
Return source-labeled geographic points for the network map: local OKW
401+
facilities unioned with Maps of Making (MoM) spaces.
402+
403+
Each point is `{id, name, lat, lon, source}` where `source` is `"local"` or
404+
`"mom"`. Local facilities without parseable coordinates are counted in
405+
`dropped_no_coords` rather than plotted. MoM is served from a 24h TTL cache
406+
and degrades gracefully — if MoM is unreachable the response is local-only
407+
with `mom_available: false`.
408+
409+
Query params:
410+
- `include_mom` (default true): set false for a local-only map.
411+
- `force_refresh` (default false): force a MoM cache refresh first.
412+
""",
413+
)
414+
async def get_okw_map(
415+
include_mom: bool = True,
416+
force_refresh: bool = False,
417+
okw_service: OKWService = Depends(get_okw_service),
418+
) -> Any:
419+
"""Return network-map points (local OKW facilities ∪ MoM spaces)."""
420+
try:
421+
data = await okw_service.get_map_points(
422+
include_mom=include_mom, force_refresh=force_refresh
423+
)
424+
return {"success": True, **data}
425+
except Exception as e:
426+
error_response = create_error_response(
427+
error=e,
428+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
429+
request_id=None,
430+
suggestion="Please try again or contact support if the issue persists",
431+
)
432+
logger.error(f"Error building OKW map points: {e}", exc_info=True)
433+
raise HTTPException(
434+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
435+
detail=error_response.model_dump(mode="json"),
436+
)
437+
438+
396439
@router.get("/{id}", response_model=OKWResponse)
397440
async def get_okw(
398441
id: UUID = Path(..., title="The ID of the OKW facility"),

0 commit comments

Comments
 (0)