jmfts-client 0.2.0__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 (32) hide show
  1. jmfts_client-0.2.0/LICENSE +21 -0
  2. jmfts_client-0.2.0/PKG-INFO +81 -0
  3. jmfts_client-0.2.0/README.md +54 -0
  4. jmfts_client-0.2.0/jmfts_client/__init__.py +91 -0
  5. jmfts_client-0.2.0/jmfts_client/_verbs.py +1979 -0
  6. jmfts_client-0.2.0/jmfts_client/contracts/__init__.py +272 -0
  7. jmfts_client-0.2.0/jmfts_client/contracts/access.py +114 -0
  8. jmfts_client-0.2.0/jmfts_client/contracts/attempt.py +158 -0
  9. jmfts_client-0.2.0/jmfts_client/contracts/conversation.py +88 -0
  10. jmfts_client-0.2.0/jmfts_client/contracts/document.py +463 -0
  11. jmfts_client-0.2.0/jmfts_client/contracts/explain.py +269 -0
  12. jmfts_client-0.2.0/jmfts_client/contracts/graph.py +206 -0
  13. jmfts_client-0.2.0/jmfts_client/contracts/index.py +64 -0
  14. jmfts_client-0.2.0/jmfts_client/contracts/ingest.py +85 -0
  15. jmfts_client-0.2.0/jmfts_client/contracts/runner.py +241 -0
  16. jmfts_client-0.2.0/jmfts_client/contracts/search.py +152 -0
  17. jmfts_client-0.2.0/jmfts_client/contracts/search_context.py +66 -0
  18. jmfts_client-0.2.0/jmfts_client/contracts/template.py +104 -0
  19. jmfts_client-0.2.0/jmfts_client/contracts/triple.py +124 -0
  20. jmfts_client-0.2.0/jmfts_client/contracts/upload.py +124 -0
  21. jmfts_client-0.2.0/jmfts_client/contracts/usetype_presentation.py +66 -0
  22. jmfts_client-0.2.0/jmfts_client/contracts/view.py +96 -0
  23. jmfts_client-0.2.0/jmfts_client/errors.py +87 -0
  24. jmfts_client-0.2.0/jmfts_client/remote.py +42 -0
  25. jmfts_client-0.2.0/jmfts_client/transport.py +166 -0
  26. jmfts_client-0.2.0/jmfts_client.egg-info/PKG-INFO +81 -0
  27. jmfts_client-0.2.0/jmfts_client.egg-info/SOURCES.txt +30 -0
  28. jmfts_client-0.2.0/jmfts_client.egg-info/dependency_links.txt +1 -0
  29. jmfts_client-0.2.0/jmfts_client.egg-info/requires.txt +5 -0
  30. jmfts_client-0.2.0/jmfts_client.egg-info/top_level.txt +1 -0
  31. jmfts_client-0.2.0/pyproject.toml +94 -0
  32. jmfts_client-0.2.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fight Fire with Fire Robotics, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: jmfts-client
3
+ Version: 0.2.0
4
+ Summary: Wire contracts and a generated HTTP client for a JMFTS retrieval appliance
5
+ Author: Fight Fire with Fire Robotics, LLC
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jmccardle/jmfts
8
+ Project-URL: Repository, https://github.com/jmccardle/jmfts
9
+ Keywords: jmfts,retrieval,client,api
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: httpx>=0.26.0
23
+ Requires-Dist: pydantic>=2.5.0
24
+ Provides-Extra: vectors
25
+ Requires-Dist: numpy>=1.26.0; extra == "vectors"
26
+ Dynamic: license-file
27
+
28
+ # jmfts-client
29
+
30
+ The thin half of [JMFTS](https://github.com/jmccardle/jmfts): the wire contracts, and an
31
+ HTTP client generated from them.
32
+
33
+ Install this to **call** a JMFTS appliance. Install `jmfts` to **run** one.
34
+
35
+ ```bash
36
+ pip install jmfts-client
37
+ ```
38
+
39
+ Two dependencies — `httpx` and `pydantic`. A consumer that only makes requests does not
40
+ install sqlalchemy, psycopg2, pgvector, transformers or pymupdf to do it.
41
+
42
+ ## Use
43
+
44
+ ```python
45
+ from jmfts_client import RemoteJmftsClient
46
+ from jmfts_client.contracts import DocumentCreate, HybridSearchRequest
47
+
48
+ with RemoteJmftsClient("http://localhost:8100", token="...") as jmfts:
49
+ doc = jmfts.create_document(DocumentCreate(title="Ada", content="Ada Lovelace"))
50
+ hits = jmfts.hybrid_search(HybridSearchRequest(query="Ada", limit=10))
51
+ for hit in hits.results:
52
+ print(hit.document_id, hit.score)
53
+ ```
54
+
55
+ Errors arrive as exceptions carrying the status: `JmftsNotFound`, `JmftsBadRequest`,
56
+ `JmftsConflict`, `JmftsUnprocessable`, `JmftsServerError`. All descend from `JmftsError`.
57
+ A request that never reached the server raises `JmftsTransportError`.
58
+
59
+ ## Where the methods come from
60
+
61
+ Nobody writes them. A JMFTS service method marked `@expose` becomes a REST route, an entry
62
+ in the OpenAPI document, a method on the in-process `LocalJmftsClient`, and a method here —
63
+ four views of one definition.
64
+
65
+ `jmfts_client/_verbs.py` is generated from the route table the server actually builds, not
66
+ from the decorator's declaration, because the decorator does not record which parameters
67
+ are body, query or path — FastAPI's inference decides that, and reading the built routes is
68
+ what keeps this client and the server agreeing about the wire. Regeneration lives in the
69
+ `jmfts` repository (`python -m scripts.generate_client`), and a test there fails if this
70
+ file falls behind the surface.
71
+
72
+ ## What this client does not offer
73
+
74
+ `LocalJmftsClient.unit_of_work()` groups several writes into one transaction. There is no
75
+ equivalent here, deliberately. Each HTTP request is its own transaction on the server, so a
76
+ `unit_of_work` over HTTP could group the calls but could not roll back the earlier ones when
77
+ a later one failed. If you need that guarantee, run in-process with `jmfts` installed.
78
+
79
+ ## Licence
80
+
81
+ MIT. Copyright (c) 2026 Fight Fire with Fire Robotics, LLC.
@@ -0,0 +1,54 @@
1
+ # jmfts-client
2
+
3
+ The thin half of [JMFTS](https://github.com/jmccardle/jmfts): the wire contracts, and an
4
+ HTTP client generated from them.
5
+
6
+ Install this to **call** a JMFTS appliance. Install `jmfts` to **run** one.
7
+
8
+ ```bash
9
+ pip install jmfts-client
10
+ ```
11
+
12
+ Two dependencies — `httpx` and `pydantic`. A consumer that only makes requests does not
13
+ install sqlalchemy, psycopg2, pgvector, transformers or pymupdf to do it.
14
+
15
+ ## Use
16
+
17
+ ```python
18
+ from jmfts_client import RemoteJmftsClient
19
+ from jmfts_client.contracts import DocumentCreate, HybridSearchRequest
20
+
21
+ with RemoteJmftsClient("http://localhost:8100", token="...") as jmfts:
22
+ doc = jmfts.create_document(DocumentCreate(title="Ada", content="Ada Lovelace"))
23
+ hits = jmfts.hybrid_search(HybridSearchRequest(query="Ada", limit=10))
24
+ for hit in hits.results:
25
+ print(hit.document_id, hit.score)
26
+ ```
27
+
28
+ Errors arrive as exceptions carrying the status: `JmftsNotFound`, `JmftsBadRequest`,
29
+ `JmftsConflict`, `JmftsUnprocessable`, `JmftsServerError`. All descend from `JmftsError`.
30
+ A request that never reached the server raises `JmftsTransportError`.
31
+
32
+ ## Where the methods come from
33
+
34
+ Nobody writes them. A JMFTS service method marked `@expose` becomes a REST route, an entry
35
+ in the OpenAPI document, a method on the in-process `LocalJmftsClient`, and a method here —
36
+ four views of one definition.
37
+
38
+ `jmfts_client/_verbs.py` is generated from the route table the server actually builds, not
39
+ from the decorator's declaration, because the decorator does not record which parameters
40
+ are body, query or path — FastAPI's inference decides that, and reading the built routes is
41
+ what keeps this client and the server agreeing about the wire. Regeneration lives in the
42
+ `jmfts` repository (`python -m scripts.generate_client`), and a test there fails if this
43
+ file falls behind the surface.
44
+
45
+ ## What this client does not offer
46
+
47
+ `LocalJmftsClient.unit_of_work()` groups several writes into one transaction. There is no
48
+ equivalent here, deliberately. Each HTTP request is its own transaction on the server, so a
49
+ `unit_of_work` over HTTP could group the calls but could not roll back the earlier ones when
50
+ a later one failed. If you need that guarantee, run in-process with `jmfts` installed.
51
+
52
+ ## Licence
53
+
54
+ MIT. Copyright (c) 2026 Fight Fire with Fire Robotics, LLC.
@@ -0,0 +1,91 @@
1
+ """JMFTS client — the wire contracts, and an HTTP client generated from them.
2
+
3
+ This distribution is the thin half of JMFTS. It carries ``httpx`` and ``pydantic`` and
4
+ nothing else, so a consumer that only needs to CALL an appliance does not install
5
+ sqlalchemy, pgvector, transformers or pymupdf to do it.
6
+
7
+ The server distribution (``jmfts``) depends on this one rather than the other way round.
8
+ That direction looks inverted and is deliberate: ``jmfts_client.contracts`` is the single
9
+ definition of every request and response shape, and both the in-process
10
+ ``LocalJmftsClient`` and this ``RemoteJmftsClient`` must validate against the SAME classes.
11
+ Shipping a copy to each side would make them equal by value and unequal by ``isinstance``
12
+ — a bug that appears only when one process holds both, which is exactly what
13
+ ``JMFTS_RUNNER_URL`` (one appliance embedding against another over HTTP) does.
14
+
15
+ Imports here are LAZY, through :pep:`562`. The server imports ``jmfts_client.contracts.*``
16
+ in dozens of modules, and an eager ``__init__`` would make every one of those build the
17
+ 94-method verb table and import ``httpx`` on the way to one Pydantic class. Laziness also
18
+ keeps the import graph shallow enough that a contract can never close a cycle back through
19
+ the client.
20
+ """
21
+
22
+ from typing import TYPE_CHECKING
23
+
24
+ __version__ = "0.2.0"
25
+
26
+ if TYPE_CHECKING: # import-time cost avoided at runtime, type checkers still see the names
27
+ from jmfts_client.errors import (
28
+ JmftsBadRequest,
29
+ JmftsConflict,
30
+ JmftsError,
31
+ JmftsForbidden,
32
+ JmftsNotFound,
33
+ JmftsResponseError,
34
+ JmftsServerError,
35
+ JmftsTransportError,
36
+ JmftsUnauthorized,
37
+ JmftsUnprocessable,
38
+ )
39
+ from jmfts_client.remote import RemoteJmftsClient
40
+ from jmfts_client.transport import DEFAULT_BASE_URL, DEFAULT_TIMEOUT
41
+
42
+ #: Public name -> the module that defines it.
43
+ _LAZY: dict[str, str] = {
44
+ "DEFAULT_BASE_URL": "jmfts_client.transport",
45
+ "DEFAULT_TIMEOUT": "jmfts_client.transport",
46
+ "JmftsBadRequest": "jmfts_client.errors",
47
+ "JmftsConflict": "jmfts_client.errors",
48
+ "JmftsError": "jmfts_client.errors",
49
+ "JmftsForbidden": "jmfts_client.errors",
50
+ "JmftsNotFound": "jmfts_client.errors",
51
+ "JmftsResponseError": "jmfts_client.errors",
52
+ "JmftsServerError": "jmfts_client.errors",
53
+ "JmftsTransportError": "jmfts_client.errors",
54
+ "JmftsUnauthorized": "jmfts_client.errors",
55
+ "JmftsUnprocessable": "jmfts_client.errors",
56
+ "RemoteJmftsClient": "jmfts_client.remote",
57
+ }
58
+
59
+
60
+ def __getattr__(name: str):
61
+ """Resolve a public name on first use. Anything unlisted is a real AttributeError."""
62
+ module_name = _LAZY.get(name)
63
+ if module_name is None:
64
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
65
+ import importlib
66
+
67
+ value = getattr(importlib.import_module(module_name), name)
68
+ globals()[name] = value # resolved once; later lookups skip this hook
69
+ return value
70
+
71
+
72
+ def __dir__() -> list[str]:
73
+ return sorted(__all__)
74
+
75
+
76
+ __all__ = [
77
+ "DEFAULT_BASE_URL",
78
+ "DEFAULT_TIMEOUT",
79
+ "JmftsBadRequest",
80
+ "JmftsConflict",
81
+ "JmftsError",
82
+ "JmftsForbidden",
83
+ "JmftsNotFound",
84
+ "JmftsResponseError",
85
+ "JmftsServerError",
86
+ "JmftsTransportError",
87
+ "JmftsUnauthorized",
88
+ "JmftsUnprocessable",
89
+ "RemoteJmftsClient",
90
+ "__version__",
91
+ ]