getbased-mcp 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/PKG-INFO +1 -1
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/getbased_mcp.egg-info/PKG-INFO +1 -1
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/getbased_mcp.egg-info/SOURCES.txt +1 -0
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/getbased_mcp.py +197 -0
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/pyproject.toml +1 -1
- getbased_mcp-0.2.4/tests/test_env_loader.py +162 -0
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/LICENSE +0 -0
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/README.md +0 -0
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/getbased_mcp.egg-info/dependency_links.txt +0 -0
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/getbased_mcp.egg-info/entry_points.txt +0 -0
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/getbased_mcp.egg-info/requires.txt +0 -0
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/getbased_mcp.egg-info/top_level.txt +0 -0
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/setup.cfg +0 -0
- {getbased_mcp-0.2.2 → getbased_mcp-0.2.4}/tests/test_tools.py +0 -0
|
@@ -21,6 +21,42 @@ import time
|
|
|
21
21
|
import httpx
|
|
22
22
|
from mcp.server.fastmcp import FastMCP
|
|
23
23
|
|
|
24
|
+
|
|
25
|
+
def _maybe_load_user_env() -> None:
|
|
26
|
+
"""Opt-in: load $XDG_CONFIG_HOME/getbased/env into os.environ.
|
|
27
|
+
|
|
28
|
+
Guarded by GETBASED_STACK_MANAGED=1 so existing deployments that wire env
|
|
29
|
+
explicitly (Hermes via ~/.hermes/config.yaml, hand-rolled setups) are
|
|
30
|
+
untouched. Uses setdefault — an explicit env var always wins over the file.
|
|
31
|
+
Silent on a missing file; malformed lines skipped without crashing.
|
|
32
|
+
Escape hatch: GETBASED_NO_ENV_FILE=1 disables even when managed.
|
|
33
|
+
"""
|
|
34
|
+
if os.environ.get("GETBASED_STACK_MANAGED") != "1":
|
|
35
|
+
return
|
|
36
|
+
if os.environ.get("GETBASED_NO_ENV_FILE") == "1":
|
|
37
|
+
return
|
|
38
|
+
xdg = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
|
|
39
|
+
path = os.path.join(xdg, "getbased", "env")
|
|
40
|
+
try:
|
|
41
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
42
|
+
lines = fh.readlines()
|
|
43
|
+
except OSError:
|
|
44
|
+
return
|
|
45
|
+
for raw in lines:
|
|
46
|
+
line = raw.strip()
|
|
47
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
48
|
+
continue
|
|
49
|
+
key, _, val = line.partition("=")
|
|
50
|
+
key = key.strip()
|
|
51
|
+
val = val.strip()
|
|
52
|
+
if len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"'):
|
|
53
|
+
val = val[1:-1]
|
|
54
|
+
if key:
|
|
55
|
+
os.environ.setdefault(key, val)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_maybe_load_user_env()
|
|
59
|
+
|
|
24
60
|
log = logging.getLogger("getbased_mcp")
|
|
25
61
|
|
|
26
62
|
mcp = FastMCP("getbased")
|
|
@@ -325,6 +361,167 @@ async def getbased_section(section: str = "", profile: str = "") -> str:
|
|
|
325
361
|
return f"[{match_key}]\n\n{sections[match_key]}"
|
|
326
362
|
|
|
327
363
|
|
|
364
|
+
@mcp.tool()
|
|
365
|
+
@_instrumented("getbased_wearables_series")
|
|
366
|
+
async def getbased_wearables_series(
|
|
367
|
+
metric: str = "",
|
|
368
|
+
days: int = 0,
|
|
369
|
+
profile: str = "",
|
|
370
|
+
) -> str:
|
|
371
|
+
"""Read the wearable daily-values series the user opted into pushing.
|
|
372
|
+
|
|
373
|
+
The user picks a window in Settings → Integrations → Agent Access:
|
|
374
|
+
7, 30, or 90 days (or off). When set, the browser pushes a
|
|
375
|
+
`[section:wearables-series-{N}d]` block to the gateway containing
|
|
376
|
+
one line per metric, daily values separated by `→` (oldest to
|
|
377
|
+
newest), `—` for no-reading days, and the primary source in parens.
|
|
378
|
+
|
|
379
|
+
This tool extracts that series and optionally slices it.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
metric: optional metric id to return only one line. Examples:
|
|
383
|
+
'hrv_rmssd' (overnight HRV), 'rhr' (overnight resting HR),
|
|
384
|
+
'hr_day' (daytime HR), 'sleep_score', 'readiness_score',
|
|
385
|
+
'steps', 'weight'. Pass empty string for the whole matrix.
|
|
386
|
+
days: optional preferred window. If 0, returns whichever
|
|
387
|
+
window the user pushed. If 7/30/90, returns that section
|
|
388
|
+
specifically (404 if not pushed). The browser only pushes
|
|
389
|
+
ONE window at a time, so non-matching values fall back.
|
|
390
|
+
profile: profile id (omit for default).
|
|
391
|
+
|
|
392
|
+
Returns the section content, or a clear error if the user hasn't
|
|
393
|
+
enabled the toggle yet.
|
|
394
|
+
"""
|
|
395
|
+
data = await _fetch_context(profile)
|
|
396
|
+
if "error" in data:
|
|
397
|
+
return f"Error: {data['error']}"
|
|
398
|
+
context = data.get("context", "")
|
|
399
|
+
if not context:
|
|
400
|
+
return "No context available"
|
|
401
|
+
|
|
402
|
+
sections = _parse_sections(context)
|
|
403
|
+
# Find the wearables-series-Nd section. Prefer requested `days`, else
|
|
404
|
+
# whichever the user opted into.
|
|
405
|
+
candidates = [k for k in sections if k.startswith("wearables-series-")]
|
|
406
|
+
if not candidates:
|
|
407
|
+
return (
|
|
408
|
+
"No wearable series available. The user can enable this in "
|
|
409
|
+
"getbased: Settings → Integrations → Agent Access → "
|
|
410
|
+
"'Push wearable daily series'. Pick 7, 30, or 90 days."
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
chosen = None
|
|
414
|
+
if days in (7, 30, 90):
|
|
415
|
+
target = f"wearables-series-{days}d"
|
|
416
|
+
chosen = next((k for k in candidates if k == target), None)
|
|
417
|
+
if not chosen:
|
|
418
|
+
available = [k.replace("wearables-series-", "").replace("d", "") for k in candidates]
|
|
419
|
+
return (
|
|
420
|
+
f"User hasn't pushed the {days}-day window. Currently "
|
|
421
|
+
f"available: {', '.join(available)} day(s). They can "
|
|
422
|
+
f"change the window in Settings → Integrations → Agent "
|
|
423
|
+
f"Access."
|
|
424
|
+
)
|
|
425
|
+
else:
|
|
426
|
+
chosen = candidates[0]
|
|
427
|
+
|
|
428
|
+
content = sections[chosen]
|
|
429
|
+
if not metric:
|
|
430
|
+
return f"[{chosen}]\n\n{content}"
|
|
431
|
+
|
|
432
|
+
# Parse one line. Lines look like:
|
|
433
|
+
# HRV (overnight) ms (oura): 33→35→32→…→39
|
|
434
|
+
metric_lower = metric.lower().strip()
|
|
435
|
+
matched = []
|
|
436
|
+
for line in content.split("\n"):
|
|
437
|
+
if not line or line.startswith("##"):
|
|
438
|
+
continue
|
|
439
|
+
# The metric id isn't directly in the line — labels are like
|
|
440
|
+
# "HRV (overnight)" / "Resting HR" / "Steps". Match by checking
|
|
441
|
+
# whether `metric_lower` appears in the line label OR the line
|
|
442
|
+
# starts with a known label-form for that metric.
|
|
443
|
+
head = line.split(":", 1)[0].lower()
|
|
444
|
+
if metric_lower in head:
|
|
445
|
+
matched.append(line)
|
|
446
|
+
continue
|
|
447
|
+
# Common id → label-fragment aliases. Browser emits labels via
|
|
448
|
+
# `${label}${unit ? ' ' + unit : ''} (${primarySource})` where
|
|
449
|
+
# `label` is `canon.label` followed by an optional `(${canon.sub})`.
|
|
450
|
+
# For `hrv_rmssd` that produces `HRV (🌙) ms (oura)` — the literal
|
|
451
|
+
# parens around the glyph mean substring matches like "hrv 🌙"
|
|
452
|
+
# FAIL. List enough fragments per id to handle all the label forms
|
|
453
|
+
# the canonical registry can emit.
|
|
454
|
+
aliases = {
|
|
455
|
+
# HRV overnight: label="HRV", sub="🌙" → "hrv (🌙)"
|
|
456
|
+
"hrv_rmssd": ["hrv (🌙)", "hrv 🌙", "hrv (overnight)", "hrv overnight"],
|
|
457
|
+
# HRV daytime: label="HRV", sub="☀️" → "hrv (☀️)"
|
|
458
|
+
"hrv_day": ["hrv (☀", "hrv ☀", "hrv (daytime)", "hrv daytime"],
|
|
459
|
+
# HRV SDNN (Apple Health): label="HRV", sub="SDNN" → "hrv (sdnn)"
|
|
460
|
+
"hrv_sdnn": ["hrv (sdnn)", "hrv sdnn"],
|
|
461
|
+
# Resting HR: label="Resting HR", sub="" → "resting hr"
|
|
462
|
+
"rhr": ["resting hr", "resting heart"],
|
|
463
|
+
# Heart rate daytime: label="Heart rate", sub="☀️" → "heart rate (☀️)"
|
|
464
|
+
"hr_day": ["heart rate (☀", "heart rate ☀", "heart rate (daytime)", "heart rate daytime"],
|
|
465
|
+
# Sleep score: label="Sleep", sub="score" → "sleep (score)"
|
|
466
|
+
"sleep_score": ["sleep (score)", "sleep score"],
|
|
467
|
+
"readiness_score": ["readiness (score)", "readiness score"],
|
|
468
|
+
"activity_score": ["activity (score)", "activity score"],
|
|
469
|
+
"stress_high_min": ["stress"],
|
|
470
|
+
"resilience_level": ["resilience"],
|
|
471
|
+
"cardio_age": ["cardio age"],
|
|
472
|
+
"strain": ["strain (day)", "strain"],
|
|
473
|
+
"steps": ["steps"],
|
|
474
|
+
"weight": ["weight"],
|
|
475
|
+
"bp_systolic": ["bp (syst)", "bp syst", "blood pressure systolic"],
|
|
476
|
+
"bp_diastolic": ["bp (dia)", "bp dia", "blood pressure diastolic"],
|
|
477
|
+
"spo2_avg": ["spo₂", "spo2"],
|
|
478
|
+
"body_temp_delta": ["body temp", "body_temp"],
|
|
479
|
+
"glucose_avg": ["glucose"],
|
|
480
|
+
# Withings full coverage (getbased PR #140 / #143). Labels are
|
|
481
|
+
# unsubbed for body comp, but sleep architecture carries subs
|
|
482
|
+
# like "Sleep total", "Sleep HR (avg) bpm", etc.
|
|
483
|
+
"pwv": ["pwv"],
|
|
484
|
+
"vascular_age": ["vascular age"],
|
|
485
|
+
"cardio_fitness": ["cardio fit"],
|
|
486
|
+
"body_fat_pct": ["body fat"],
|
|
487
|
+
"fat_mass_kg": ["fat mass"],
|
|
488
|
+
"muscle_mass_kg": ["muscle"],
|
|
489
|
+
"lean_mass_kg": ["lean mass"],
|
|
490
|
+
"bone_mass_kg": ["bone"],
|
|
491
|
+
"water_mass_kg": ["water"],
|
|
492
|
+
"visceral_fat": ["visceral fat"],
|
|
493
|
+
"nerve_health_score": ["nerve health"],
|
|
494
|
+
"body_temp": ["body temp"],
|
|
495
|
+
"skin_temp": ["skin temp"],
|
|
496
|
+
"sleep_total_min": ["sleep total"],
|
|
497
|
+
"sleep_deep_min": ["deep sleep"],
|
|
498
|
+
"sleep_light_min": ["light sleep"],
|
|
499
|
+
"sleep_rem_min": ["rem sleep"],
|
|
500
|
+
"sleep_awake_min": ["awake (in bed)", "awake in bed"],
|
|
501
|
+
"sleep_hr_avg": ["sleep hr (avg)", "sleep hr"],
|
|
502
|
+
"sleep_breathing_rate": ["breathing (sleep)", "breathing"],
|
|
503
|
+
"sleep_snoring_min": ["snoring"],
|
|
504
|
+
"sleep_breath_disturb": ["apnea (level)", "apnea"],
|
|
505
|
+
}
|
|
506
|
+
for alias_id, label_forms in aliases.items():
|
|
507
|
+
if alias_id == metric_lower and any(lf in head for lf in label_forms):
|
|
508
|
+
matched.append(line)
|
|
509
|
+
break
|
|
510
|
+
|
|
511
|
+
if not matched:
|
|
512
|
+
# Surface the available metric labels so the agent can retry.
|
|
513
|
+
labels = []
|
|
514
|
+
for line in content.split("\n"):
|
|
515
|
+
if line and not line.startswith("##") and ":" in line:
|
|
516
|
+
labels.append(line.split(":", 1)[0].strip())
|
|
517
|
+
return (
|
|
518
|
+
f"Metric '{metric}' not found in [{chosen}]. "
|
|
519
|
+
f"Available labels: {' · '.join(labels)}"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
return f"[{chosen}: {metric}]\n\n" + "\n".join(matched)
|
|
523
|
+
|
|
524
|
+
|
|
328
525
|
@mcp.tool()
|
|
329
526
|
@_instrumented("getbased_list_profiles")
|
|
330
527
|
async def getbased_list_profiles() -> str:
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Tests for _maybe_load_user_env in getbased_mcp.
|
|
2
|
+
|
|
3
|
+
This is the critical guardrail for existing deployments like Hermes: without
|
|
4
|
+
GETBASED_STACK_MANAGED=1, the loader MUST be a no-op — no filesystem access,
|
|
5
|
+
no env mutation. Hermes doesn't set the flag and doesn't use the shared env
|
|
6
|
+
file, so these tests protect its MCP behavior from any regression.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def env_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
18
|
+
"""Isolated XDG_CONFIG_HOME with a getbased/env file containing a
|
|
19
|
+
sentinel value. Tests decide whether the loader sees it by toggling
|
|
20
|
+
GETBASED_STACK_MANAGED / GETBASED_NO_ENV_FILE."""
|
|
21
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
22
|
+
d = tmp_path / "getbased"
|
|
23
|
+
d.mkdir()
|
|
24
|
+
path = d / "env"
|
|
25
|
+
path.write_text("GETBASED_TOKEN=from_file\nLENS_URL=http://from-file:9999\n")
|
|
26
|
+
return path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load(monkeypatch: pytest.MonkeyPatch):
|
|
30
|
+
"""Freshly import and return the loader function. Reload ensures we
|
|
31
|
+
don't accidentally share state between tests."""
|
|
32
|
+
import importlib
|
|
33
|
+
|
|
34
|
+
import getbased_mcp
|
|
35
|
+
|
|
36
|
+
importlib.reload(getbased_mcp)
|
|
37
|
+
return getbased_mcp._maybe_load_user_env
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_noop_without_managed_flag(env_file, monkeypatch):
|
|
41
|
+
"""Hermes-safety contract: no flag → no read, no mutation."""
|
|
42
|
+
monkeypatch.delenv("GETBASED_STACK_MANAGED", raising=False)
|
|
43
|
+
monkeypatch.delenv("GETBASED_TOKEN", raising=False)
|
|
44
|
+
monkeypatch.delenv("LENS_URL", raising=False)
|
|
45
|
+
|
|
46
|
+
loader = _load(monkeypatch)
|
|
47
|
+
loader()
|
|
48
|
+
|
|
49
|
+
assert "GETBASED_TOKEN" not in os.environ
|
|
50
|
+
assert "LENS_URL" not in os.environ
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_loads_when_managed(env_file, monkeypatch):
|
|
54
|
+
monkeypatch.setenv("GETBASED_STACK_MANAGED", "1")
|
|
55
|
+
monkeypatch.delenv("GETBASED_TOKEN", raising=False)
|
|
56
|
+
monkeypatch.delenv("LENS_URL", raising=False)
|
|
57
|
+
|
|
58
|
+
loader = _load(monkeypatch)
|
|
59
|
+
loader()
|
|
60
|
+
|
|
61
|
+
assert os.environ["GETBASED_TOKEN"] == "from_file"
|
|
62
|
+
assert os.environ["LENS_URL"] == "http://from-file:9999"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_explicit_env_wins_over_file(env_file, monkeypatch):
|
|
66
|
+
"""setdefault semantics — user/systemd-provided env must not be
|
|
67
|
+
overridden by the shared file."""
|
|
68
|
+
monkeypatch.setenv("GETBASED_STACK_MANAGED", "1")
|
|
69
|
+
monkeypatch.setenv("GETBASED_TOKEN", "from_shell")
|
|
70
|
+
|
|
71
|
+
loader = _load(monkeypatch)
|
|
72
|
+
loader()
|
|
73
|
+
|
|
74
|
+
assert os.environ["GETBASED_TOKEN"] == "from_shell"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_escape_hatch(env_file, monkeypatch):
|
|
78
|
+
"""GETBASED_NO_ENV_FILE=1 bails even when managed — debugging/override use."""
|
|
79
|
+
monkeypatch.setenv("GETBASED_STACK_MANAGED", "1")
|
|
80
|
+
monkeypatch.setenv("GETBASED_NO_ENV_FILE", "1")
|
|
81
|
+
monkeypatch.delenv("GETBASED_TOKEN", raising=False)
|
|
82
|
+
|
|
83
|
+
loader = _load(monkeypatch)
|
|
84
|
+
loader()
|
|
85
|
+
|
|
86
|
+
assert "GETBASED_TOKEN" not in os.environ
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_missing_file_silent(tmp_path, monkeypatch):
|
|
90
|
+
"""No env file at the expected path → no error, no mutation."""
|
|
91
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
92
|
+
monkeypatch.setenv("GETBASED_STACK_MANAGED", "1")
|
|
93
|
+
monkeypatch.delenv("GETBASED_TOKEN", raising=False)
|
|
94
|
+
|
|
95
|
+
loader = _load(monkeypatch)
|
|
96
|
+
loader() # must not raise
|
|
97
|
+
|
|
98
|
+
assert "GETBASED_TOKEN" not in os.environ
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_malformed_lines_skipped(tmp_path, monkeypatch):
|
|
102
|
+
"""A typo'd env file must not crash MCP startup."""
|
|
103
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
104
|
+
d = tmp_path / "getbased"
|
|
105
|
+
d.mkdir()
|
|
106
|
+
(d / "env").write_text(
|
|
107
|
+
"# a comment\n"
|
|
108
|
+
"\n"
|
|
109
|
+
"NO_EQUALS_SIGN\n"
|
|
110
|
+
"GOOD_VAR=ok\n"
|
|
111
|
+
" =missing_key\n"
|
|
112
|
+
)
|
|
113
|
+
monkeypatch.setenv("GETBASED_STACK_MANAGED", "1")
|
|
114
|
+
monkeypatch.delenv("GOOD_VAR", raising=False)
|
|
115
|
+
|
|
116
|
+
loader = _load(monkeypatch)
|
|
117
|
+
loader()
|
|
118
|
+
|
|
119
|
+
assert os.environ["GOOD_VAR"] == "ok"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_quoted_values_unwrapped(tmp_path, monkeypatch):
|
|
123
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
124
|
+
d = tmp_path / "getbased"
|
|
125
|
+
d.mkdir()
|
|
126
|
+
(d / "env").write_text('DOUBLE="dv"\nSINGLE=\'sv\'\nBARE=bv\n')
|
|
127
|
+
monkeypatch.setenv("GETBASED_STACK_MANAGED", "1")
|
|
128
|
+
for k in ("DOUBLE", "SINGLE", "BARE"):
|
|
129
|
+
monkeypatch.delenv(k, raising=False)
|
|
130
|
+
|
|
131
|
+
loader = _load(monkeypatch)
|
|
132
|
+
loader()
|
|
133
|
+
|
|
134
|
+
assert os.environ["DOUBLE"] == "dv"
|
|
135
|
+
assert os.environ["SINGLE"] == "sv"
|
|
136
|
+
assert os.environ["BARE"] == "bv"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_module_import_parity_without_flag(monkeypatch, tmp_path):
|
|
140
|
+
"""Smoke: importing getbased_mcp without the managed flag must not
|
|
141
|
+
read or mutate anything related to the shared env file. This is the
|
|
142
|
+
end-to-end Hermes contract — if this ever fails, we risk regressing
|
|
143
|
+
a live deployment."""
|
|
144
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
145
|
+
d = tmp_path / "getbased"
|
|
146
|
+
d.mkdir()
|
|
147
|
+
# Would poison LENS_URL if the loader ran
|
|
148
|
+
(d / "env").write_text("LENS_URL=http://POISON:1234\n")
|
|
149
|
+
|
|
150
|
+
monkeypatch.delenv("GETBASED_STACK_MANAGED", raising=False)
|
|
151
|
+
monkeypatch.setenv("LENS_URL", "http://explicit:8322")
|
|
152
|
+
monkeypatch.setenv("GETBASED_TOKEN", "real")
|
|
153
|
+
|
|
154
|
+
import importlib
|
|
155
|
+
|
|
156
|
+
import getbased_mcp
|
|
157
|
+
|
|
158
|
+
importlib.reload(getbased_mcp)
|
|
159
|
+
|
|
160
|
+
# Explicit env must survive; poison must not have been loaded
|
|
161
|
+
assert getbased_mcp.LENS_URL == "http://explicit:8322"
|
|
162
|
+
assert "POISON" not in getbased_mcp.LENS_URL
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|