needlepath 0.1.0__tar.gz → 0.1.1__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.
Files changed (24) hide show
  1. {needlepath-0.1.0 → needlepath-0.1.1}/PKG-INFO +2 -2
  2. {needlepath-0.1.0 → needlepath-0.1.1}/README.md +1 -1
  3. {needlepath-0.1.0 → needlepath-0.1.1}/pyproject.toml +1 -1
  4. {needlepath-0.1.0 → needlepath-0.1.1}/src/needlepath/client.py +1 -1
  5. {needlepath-0.1.0 → needlepath-0.1.1}/src/needlepath/models.py +14 -4
  6. {needlepath-0.1.0 → needlepath-0.1.1}/tests/test_config_and_guards.py +1 -1
  7. {needlepath-0.1.0 → needlepath-0.1.1}/tests/test_wire_compatibility.py +21 -1
  8. {needlepath-0.1.0 → needlepath-0.1.1}/.gitignore +0 -0
  9. {needlepath-0.1.0 → needlepath-0.1.1}/LICENSE +0 -0
  10. {needlepath-0.1.0 → needlepath-0.1.1}/src/needlepath/__init__.py +0 -0
  11. {needlepath-0.1.0 → needlepath-0.1.1}/src/needlepath/errors.py +0 -0
  12. {needlepath-0.1.0 → needlepath-0.1.1}/src/needlepath/features.py +0 -0
  13. {needlepath-0.1.0 → needlepath-0.1.1}/src/needlepath/py.typed +0 -0
  14. {needlepath-0.1.0 → needlepath-0.1.1}/src/needlepath/result.py +0 -0
  15. {needlepath-0.1.0 → needlepath-0.1.1}/src/needlepath/testing.py +0 -0
  16. {needlepath-0.1.0 → needlepath-0.1.1}/src/needlepath/transport.py +0 -0
  17. {needlepath-0.1.0 → needlepath-0.1.1}/src/needlepath/version.py +0 -0
  18. {needlepath-0.1.0 → needlepath-0.1.1}/tests/test_async_parity.py +0 -0
  19. {needlepath-0.1.0 → needlepath-0.1.1}/tests/test_fail_open.py +0 -0
  20. {needlepath-0.1.0 → needlepath-0.1.1}/tests/test_live_endpoint.py +0 -0
  21. {needlepath-0.1.0 → needlepath-0.1.1}/tests/test_no_secret_leak.py +0 -0
  22. {needlepath-0.1.0 → needlepath-0.1.1}/tests/test_retries.py +0 -0
  23. {needlepath-0.1.0 → needlepath-0.1.1}/tests/test_shadow_mode.py +0 -0
  24. {needlepath-0.1.0 → needlepath-0.1.1}/tests/test_urllib_transport.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: needlepath
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Framework-free client for the Needlepath hosted context-selection service.
5
5
  Project-URL: Homepage, https://nextmoca.com
6
6
  Author: Next Moca Global, Inc.
@@ -94,7 +94,7 @@ never blocks the event loop either way.
94
94
  |---|---|---|
95
95
  | `api_key` | `NEEDLEPATH_API_KEY` | none (no `Authorization` header) |
96
96
  | `operating_point` | `NEEDLEPATH_OPERATING_POINT` | **required** |
97
- | `base_url` | `NEEDLEPATH_BASE_URL` | `https://ayf02egtaf.execute-api.us-east-2.amazonaws.com/prod` |
97
+ | `base_url` | `NEEDLEPATH_BASE_URL` | `https://api.nextmoca.com` |
98
98
  | `timeout` | — | `10.0` s, total, retries included |
99
99
  | `max_retries` | — | `2` |
100
100
  | `max_request_bytes` | — | `6 * 1024 * 1024` |
@@ -65,7 +65,7 @@ never blocks the event loop either way.
65
65
  |---|---|---|
66
66
  | `api_key` | `NEEDLEPATH_API_KEY` | none (no `Authorization` header) |
67
67
  | `operating_point` | `NEEDLEPATH_OPERATING_POINT` | **required** |
68
- | `base_url` | `NEEDLEPATH_BASE_URL` | `https://ayf02egtaf.execute-api.us-east-2.amazonaws.com/prod` |
68
+ | `base_url` | `NEEDLEPATH_BASE_URL` | `https://api.nextmoca.com` |
69
69
  | `timeout` | — | `10.0` s, total, retries included |
70
70
  | `max_retries` | — | `2` |
71
71
  | `max_request_bytes` | — | `6 * 1024 * 1024` |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "needlepath"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Framework-free client for the Needlepath hosted context-selection service."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -62,7 +62,7 @@ from .version import __version__
62
62
  #: follow-through (§5, S-17a); until it exists this is the address published runs
63
63
  #: and design partners use, and it stays overridable by argument or by
64
64
  #: ``NEEDLEPATH_BASE_URL`` so that swapping it is a config change, not a release.
65
- DEFAULT_BASE_URL = "https://ayf02egtaf.execute-api.us-east-2.amazonaws.com/prod"
65
+ DEFAULT_BASE_URL = "https://api.nextmoca.com"
66
66
 
67
67
  SELECT_PATH = "/v1/context/select"
68
68
  HEALTH_PATH = "/v1/health"
@@ -90,24 +90,34 @@ class ContextRecord:
90
90
  source: Optional[str] = None
91
91
  title: Optional[str] = None
92
92
  step_id: Optional[str] = None
93
- importance: float = 0.0
93
+ # NEXPE-437: None means "the caller never annotated importance", and the
94
+ # field is then OMITTED from the wire so the server applies its neutral
95
+ # default. The old `0.0` default was sent explicitly, which the server
96
+ # cannot tell apart from a deliberate 0.0 -- and 0.0 stands the record
97
+ # down. An explicit `importance=0.0` still goes on the wire as 0.0.
98
+ importance: Optional[float] = None
94
99
  keywords: List[str] = field(default_factory=list)
95
100
  tags: List[str] = field(default_factory=list)
96
101
  attributes: Dict[str, Any] = field(default_factory=dict)
97
102
 
98
103
  def to_wire(self) -> Dict[str, Any]:
99
- return {
104
+ wire = {
100
105
  "text": self.text,
101
106
  "kind": self.kind,
102
107
  "id": self.id,
103
108
  "source": self.source,
104
109
  "title": self.title,
105
110
  "step_id": self.step_id,
106
- "importance": self.importance,
107
111
  "keywords": list(self.keywords),
108
112
  "tags": list(self.tags),
109
113
  "attributes": dict(self.attributes),
110
114
  }
115
+ # NEXPE-437: omitted, not null. The server reads importance with
116
+ # `float(d.get("importance", ...))`, so `null` would be a crash and
117
+ # an always-present key would erase the unset/explicit distinction.
118
+ if self.importance is not None:
119
+ wire["importance"] = self.importance
120
+ return wire
111
121
 
112
122
  @classmethod
113
123
  def from_wire(cls, d: Dict[str, Any]) -> ContextRecord:
@@ -118,7 +128,7 @@ class ContextRecord:
118
128
  source=d.get("source"),
119
129
  title=d.get("title"),
120
130
  step_id=d.get("step_id"),
121
- importance=float(d.get("importance", 0.0)),
131
+ importance=float(d["importance"]) if d.get("importance") is not None else None,
122
132
  keywords=list(d.get("keywords", [])),
123
133
  tags=list(d.get("tags", [])),
124
134
  attributes=dict(d.get("attributes", {})),
@@ -185,7 +185,7 @@ def test_base_url_comes_from_the_environment(monkeypatch):
185
185
  def test_the_documented_default_is_what_ships():
186
186
  from needlepath import DEFAULT_BASE_URL
187
187
 
188
- assert DEFAULT_BASE_URL == "https://ayf02egtaf.execute-api.us-east-2.amazonaws.com/prod"
188
+ assert DEFAULT_BASE_URL == "https://api.nextmoca.com"
189
189
 
190
190
 
191
191
  # --------------------------------------------------------------------------- #
@@ -182,7 +182,11 @@ def test_to_wire_emits_the_full_shape_including_nones():
182
182
  )
183
183
  wire = request.to_wire()
184
184
  assert set(wire) == PUBLISHED["ContextRequest"]
185
- assert set(wire["records"][0]) == PUBLISHED["ContextRecord"]
185
+ # NEXPE-437: `importance` is the one deliberate exception to the
186
+ # full-shape rule. Unset means OMITTED from the wire, so the server can
187
+ # tell "never annotated" (neutral) apart from an explicit 0.0
188
+ # (stand-down). Every other optional stays present-as-null.
189
+ assert set(wire["records"][0]) == PUBLISHED["ContextRecord"] - {"importance"}
186
190
  assert set(wire["task"]) == PUBLISHED["TaskSpec"]
187
191
  assert set(wire["budget"]) == PUBLISHED["BudgetSpec"]
188
192
  assert wire["records"][0]["source"] is None # present, not omitted
@@ -190,6 +194,22 @@ def test_to_wire_emits_the_full_shape_including_nones():
190
194
  json.dumps(wire) # and it is serializable
191
195
 
192
196
 
197
+ def test_unset_importance_is_omitted_and_explicit_zero_is_sent():
198
+ """NEXPE-437: the wire distinguishes unannotated from deliberately 0.0."""
199
+ unset = models.ContextRecord(text="t").to_wire()
200
+ assert "importance" not in unset
201
+
202
+ explicit_zero = models.ContextRecord(text="t", importance=0.0).to_wire()
203
+ assert explicit_zero["importance"] == 0.0
204
+
205
+ explicit = models.ContextRecord(text="t", importance=7.5).to_wire()
206
+ assert explicit["importance"] == 7.5
207
+
208
+ # And from_wire preserves the same distinction on the way back in.
209
+ assert models.ContextRecord.from_wire({"text": "t"}).importance is None
210
+ assert models.ContextRecord.from_wire({"text": "t", "importance": 0.0}).importance == 0.0
211
+
212
+
193
213
  def test_unknown_response_fields_are_kept_not_rejected():
194
214
  """`/v1` is additive (API_VERSIONING.md). `usage` is already scheduled to
195
215
  arrive this way (E2), and a client that 'validates' by rejecting unknown
File without changes
File without changes