moveezi 1.0.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.
moveezi-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: moveezi
3
+ Version: 1.0.0
4
+ Summary: Client for the public Moveezi API: read moveezi.com as structured data, search it, and reach the sales team. No credentials needed.
5
+ Author-email: Dumont Pty Ltd <sales@moveezi.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://www.moveezi.com/developers
8
+ Project-URL: Documentation, https://www.moveezi.com/openapi.json
9
+ Project-URL: Repository, https://github.com/DumontAI/moveezi-sdk
10
+ Project-URL: Issues, https://github.com/DumontAI/moveezi-sdk/issues
11
+ Keywords: moveezi,moving,relocation,storage,logistics,api,sdk,mcp,agent,llm
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Office/Business
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Moveezi SDK
21
+
22
+ Client libraries and a CLI for the public [Moveezi](https://www.moveezi.com/) API.
23
+
24
+ [Moveezi](https://www.moveezi.com/) is the operating system for moving, relocation and
25
+ storage companies: sales and quoting, on-site and video surveys, dispatch and crew
26
+ scheduling, a mobile crew app, warehousing, sea/air/road shipment tracking, a live control
27
+ tower and per-job profit and loss.
28
+
29
+ **There is nothing to authenticate with.** No key, no signup, no token. Every read is open.
30
+ The one write, `submitLead`, is the same endpoint the website's own forms post to, and it
31
+ has a sandbox so you can check a payload without creating anything.
32
+
33
+ - API reference: <https://www.moveezi.com/openapi.json>
34
+ - Developer portal: <https://www.moveezi.com/developers>
35
+ - MCP server: `https://www.moveezi.com/mcp` (Streamable HTTP, no credentials)
36
+ - Authentication, in one page: <https://www.moveezi.com/auth.md>
37
+
38
+ ## Install
39
+
40
+ **JavaScript / TypeScript** (Node 18+, zero dependencies):
41
+
42
+ ```bash
43
+ npm install moveezi
44
+ # or straight from source, no registry account needed:
45
+ npm install DumontAI/moveezi-sdk
46
+ ```
47
+
48
+ **Python** (3.9+, standard library only):
49
+
50
+ ```bash
51
+ pip install moveezi
52
+ ```
53
+
54
+ **CLI**, without installing anything:
55
+
56
+ ```bash
57
+ npx moveezi search "video survey"
58
+ ```
59
+
60
+ ## Use it
61
+
62
+ ```js
63
+ import { MoveeziClient } from "moveezi";
64
+
65
+ const moveezi = new MoveeziClient();
66
+
67
+ const hits = await moveezi.searchSite("video survey", { limit: 3 });
68
+ const pricing = await moveezi.getPage("pricing", { format: "markdown" });
69
+ const pages = await moveezi.allPages({ prefix: "blog/" });
70
+
71
+ // Check a lead before you send one. Creates nothing.
72
+ const check = await moveezi.validateLead({ name: "Ada", email: "ada@example.com" });
73
+ if (check.wouldSubmit) {
74
+ await moveezi.submitLead(
75
+ { name: "Ada", email: "ada@example.com", intent: "demo" },
76
+ { idempotencyKey: crypto.randomUUID() }, // a retry will not create a second lead
77
+ );
78
+ }
79
+ ```
80
+
81
+ ```python
82
+ from moveezi import Moveezi
83
+
84
+ moveezi = Moveezi()
85
+ hits = moveezi.search("video survey", limit=3)
86
+ pricing = moveezi.get_page("pricing", markdown=True)
87
+ pages = list(moveezi.all_pages(prefix="blog/"))
88
+
89
+ check = moveezi.validate_lead({"name": "Ada", "email": "ada@example.com"})
90
+ if check["wouldSubmit"]:
91
+ moveezi.submit_lead({"name": "Ada", "email": "ada@example.com"},
92
+ idempotency_key="a3f1c9e2")
93
+ ```
94
+
95
+ ## CLI
96
+
97
+ ```
98
+ moveezi search <query> Search moveezi.com
99
+ moveezi page <slug> One page as markdown
100
+ moveezi pages [--prefix] Every published page
101
+ moveezi batch <slug...> Several pages in one request
102
+ moveezi pricing The pricing page
103
+ moveezi lead [--submit] Check a lead; only sends with --submit
104
+ moveezi openapi The OpenAPI 3.1 document
105
+ moveezi auth How to authenticate here (you do not)
106
+ moveezi policy Versioning and deprecation policy
107
+ moveezi mcp MCP endpoint and client config
108
+ ```
109
+
110
+ ## Rate limits
111
+
112
+ Reads are 120 requests per minute per IP. `POST /api/lead` is 5 per hour per IP. Both
113
+ report `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and `RateLimit-Policy`
114
+ on every response, and `Retry-After` on a 429. Both clients expose them:
115
+
116
+ ```js
117
+ await moveezi.listPages();
118
+ moveezi.lastRateLimit; // { limit: 120, remaining: 119, reset: 60 }
119
+ ```
120
+
121
+ A 429 throws a `MoveeziError` with `retryAfter` in seconds, so you can back off on real
122
+ numbers rather than guessing.
123
+
124
+ ## If you are an AI agent
125
+
126
+ You may not need this package at all. The API is plain HTTP with an
127
+ [OpenAPI 3.1 document](https://www.moveezi.com/openapi.json), every page is available as
128
+ markdown (append `.md`, or send `Accept: text/markdown`), and there is an
129
+ [MCP server](https://www.moveezi.com/mcp) with tools for search, page retrieval and
130
+ pricing. Start at [llms.txt](https://www.moveezi.com/llms.txt).
131
+
132
+ Please only call `submitLead` on behalf of a person who asked you to contact Moveezi, and
133
+ send an `Idempotency-Key` so a retry does not create a duplicate. `validateLead` is there
134
+ so you never have to learn the rules by creating a bad record.
135
+
136
+ ## Stability
137
+
138
+ The version is in the path (`/api/v1`) and this API changes additively within it. Anything
139
+ being withdrawn gets `Deprecation` and `Sunset` headers and six months' notice first. Full
140
+ policy: <https://www.moveezi.com/api-deprecation-policy>.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ npm test # JavaScript, no network
146
+ MOVEEZI_LIVE=1 npm test # plus a couple against production
147
+ cd python && python3 -m unittest test_moveezi
148
+ ```
149
+
150
+ ## Licence
151
+
152
+ MIT. See [LICENSE](LICENSE). The API and the content it serves belong to Dumont Pty Ltd.
@@ -0,0 +1,133 @@
1
+ # Moveezi SDK
2
+
3
+ Client libraries and a CLI for the public [Moveezi](https://www.moveezi.com/) API.
4
+
5
+ [Moveezi](https://www.moveezi.com/) is the operating system for moving, relocation and
6
+ storage companies: sales and quoting, on-site and video surveys, dispatch and crew
7
+ scheduling, a mobile crew app, warehousing, sea/air/road shipment tracking, a live control
8
+ tower and per-job profit and loss.
9
+
10
+ **There is nothing to authenticate with.** No key, no signup, no token. Every read is open.
11
+ The one write, `submitLead`, is the same endpoint the website's own forms post to, and it
12
+ has a sandbox so you can check a payload without creating anything.
13
+
14
+ - API reference: <https://www.moveezi.com/openapi.json>
15
+ - Developer portal: <https://www.moveezi.com/developers>
16
+ - MCP server: `https://www.moveezi.com/mcp` (Streamable HTTP, no credentials)
17
+ - Authentication, in one page: <https://www.moveezi.com/auth.md>
18
+
19
+ ## Install
20
+
21
+ **JavaScript / TypeScript** (Node 18+, zero dependencies):
22
+
23
+ ```bash
24
+ npm install moveezi
25
+ # or straight from source, no registry account needed:
26
+ npm install DumontAI/moveezi-sdk
27
+ ```
28
+
29
+ **Python** (3.9+, standard library only):
30
+
31
+ ```bash
32
+ pip install moveezi
33
+ ```
34
+
35
+ **CLI**, without installing anything:
36
+
37
+ ```bash
38
+ npx moveezi search "video survey"
39
+ ```
40
+
41
+ ## Use it
42
+
43
+ ```js
44
+ import { MoveeziClient } from "moveezi";
45
+
46
+ const moveezi = new MoveeziClient();
47
+
48
+ const hits = await moveezi.searchSite("video survey", { limit: 3 });
49
+ const pricing = await moveezi.getPage("pricing", { format: "markdown" });
50
+ const pages = await moveezi.allPages({ prefix: "blog/" });
51
+
52
+ // Check a lead before you send one. Creates nothing.
53
+ const check = await moveezi.validateLead({ name: "Ada", email: "ada@example.com" });
54
+ if (check.wouldSubmit) {
55
+ await moveezi.submitLead(
56
+ { name: "Ada", email: "ada@example.com", intent: "demo" },
57
+ { idempotencyKey: crypto.randomUUID() }, // a retry will not create a second lead
58
+ );
59
+ }
60
+ ```
61
+
62
+ ```python
63
+ from moveezi import Moveezi
64
+
65
+ moveezi = Moveezi()
66
+ hits = moveezi.search("video survey", limit=3)
67
+ pricing = moveezi.get_page("pricing", markdown=True)
68
+ pages = list(moveezi.all_pages(prefix="blog/"))
69
+
70
+ check = moveezi.validate_lead({"name": "Ada", "email": "ada@example.com"})
71
+ if check["wouldSubmit"]:
72
+ moveezi.submit_lead({"name": "Ada", "email": "ada@example.com"},
73
+ idempotency_key="a3f1c9e2")
74
+ ```
75
+
76
+ ## CLI
77
+
78
+ ```
79
+ moveezi search <query> Search moveezi.com
80
+ moveezi page <slug> One page as markdown
81
+ moveezi pages [--prefix] Every published page
82
+ moveezi batch <slug...> Several pages in one request
83
+ moveezi pricing The pricing page
84
+ moveezi lead [--submit] Check a lead; only sends with --submit
85
+ moveezi openapi The OpenAPI 3.1 document
86
+ moveezi auth How to authenticate here (you do not)
87
+ moveezi policy Versioning and deprecation policy
88
+ moveezi mcp MCP endpoint and client config
89
+ ```
90
+
91
+ ## Rate limits
92
+
93
+ Reads are 120 requests per minute per IP. `POST /api/lead` is 5 per hour per IP. Both
94
+ report `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and `RateLimit-Policy`
95
+ on every response, and `Retry-After` on a 429. Both clients expose them:
96
+
97
+ ```js
98
+ await moveezi.listPages();
99
+ moveezi.lastRateLimit; // { limit: 120, remaining: 119, reset: 60 }
100
+ ```
101
+
102
+ A 429 throws a `MoveeziError` with `retryAfter` in seconds, so you can back off on real
103
+ numbers rather than guessing.
104
+
105
+ ## If you are an AI agent
106
+
107
+ You may not need this package at all. The API is plain HTTP with an
108
+ [OpenAPI 3.1 document](https://www.moveezi.com/openapi.json), every page is available as
109
+ markdown (append `.md`, or send `Accept: text/markdown`), and there is an
110
+ [MCP server](https://www.moveezi.com/mcp) with tools for search, page retrieval and
111
+ pricing. Start at [llms.txt](https://www.moveezi.com/llms.txt).
112
+
113
+ Please only call `submitLead` on behalf of a person who asked you to contact Moveezi, and
114
+ send an `Idempotency-Key` so a retry does not create a duplicate. `validateLead` is there
115
+ so you never have to learn the rules by creating a bad record.
116
+
117
+ ## Stability
118
+
119
+ The version is in the path (`/api/v1`) and this API changes additively within it. Anything
120
+ being withdrawn gets `Deprecation` and `Sunset` headers and six months' notice first. Full
121
+ policy: <https://www.moveezi.com/api-deprecation-policy>.
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ npm test # JavaScript, no network
127
+ MOVEEZI_LIVE=1 npm test # plus a couple against production
128
+ cd python && python3 -m unittest test_moveezi
129
+ ```
130
+
131
+ ## Licence
132
+
133
+ MIT. See [LICENSE](LICENSE). The API and the content it serves belong to Dumont Pty Ltd.
@@ -0,0 +1,189 @@
1
+ """Client for the public Moveezi API.
2
+
3
+ There is nothing to authenticate with: every read is open, and the one write
4
+ (``submit_lead``) is the same endpoint the website's own forms post to. The
5
+ description this mirrors is at https://www.moveezi.com/openapi.json.
6
+
7
+ Standard library only, so it installs anywhere without pulling a dependency
8
+ tree behind it.
9
+
10
+ >>> from moveezi import Moveezi
11
+ >>> Moveezi().search("video survey")["count"] # doctest: +SKIP
12
+ 5
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import urllib.error
18
+ import urllib.parse
19
+ import urllib.request
20
+ from typing import Any, Dict, Iterator, List, Optional
21
+
22
+ __all__ = ["Moveezi", "MoveeziError", "MCP_ENDPOINT", "MCP_MANIFEST", "__version__"]
23
+
24
+ __version__ = "1.0.0"
25
+
26
+ BASE = "https://www.moveezi.com"
27
+ MCP_ENDPOINT = BASE + "/mcp"
28
+ MCP_MANIFEST = BASE + "/.well-known/mcp.json"
29
+ _UA = "moveezi-sdk-python/%s (+https://www.moveezi.com/developers)" % __version__
30
+
31
+
32
+ class MoveeziError(Exception):
33
+ """Any non-2xx response, carrying what the API said and its limits."""
34
+
35
+ def __init__(self, message: str, status: Optional[int] = None,
36
+ body: Any = None, rate_limit: Optional[Dict[str, int]] = None,
37
+ retry_after: Optional[int] = None) -> None:
38
+ super().__init__(message)
39
+ self.status = status
40
+ self.body = body
41
+ self.rate_limit = rate_limit
42
+ #: Seconds to wait. Present on a 429.
43
+ self.retry_after = retry_after
44
+
45
+
46
+ def _rate_limit(headers) -> Optional[Dict[str, int]]:
47
+ limit = headers.get("RateLimit-Limit")
48
+ if limit is None:
49
+ return None
50
+ out = {"limit": int(limit)}
51
+ for key, name in (("remaining", "RateLimit-Remaining"), ("reset", "RateLimit-Reset")):
52
+ value = headers.get(name)
53
+ if value is not None:
54
+ out[key] = int(value)
55
+ return out
56
+
57
+
58
+ class Moveezi:
59
+ """The public Moveezi API. No credentials, no setup."""
60
+
61
+ def __init__(self, base_url: str = BASE, timeout: float = 30.0,
62
+ user_agent: str = _UA) -> None:
63
+ self.base_url = base_url.rstrip("/")
64
+ self.timeout = timeout
65
+ self.user_agent = user_agent
66
+ #: Rate-limit headers from the most recent response.
67
+ self.last_rate_limit: Optional[Dict[str, int]] = None
68
+
69
+ # -- plumbing ---------------------------------------------------------
70
+ def _request(self, path: str, method: str = "GET", body: Any = None,
71
+ accept: str = "application/json") -> Any:
72
+ data = None if body is None else json.dumps(body).encode("utf-8")
73
+ headers = {"Accept": accept, "User-Agent": self.user_agent}
74
+ if data is not None:
75
+ headers["Content-Type"] = "application/json"
76
+ request = urllib.request.Request(self.base_url + path, data=data,
77
+ headers=headers, method=method)
78
+ try:
79
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
80
+ self.last_rate_limit = _rate_limit(response.headers)
81
+ raw = response.read().decode("utf-8")
82
+ ctype = response.headers.get("Content-Type", "")
83
+ return json.loads(raw) if "json" in ctype else raw
84
+ except urllib.error.HTTPError as err:
85
+ raw = err.read().decode("utf-8", "replace")
86
+ try:
87
+ payload = json.loads(raw)
88
+ except ValueError:
89
+ payload = raw
90
+ self.last_rate_limit = _rate_limit(err.headers)
91
+ # 422 from the sandbox is the answer the caller asked for, not a failure.
92
+ if err.code == 422 and path.startswith("/api/v1/lead/validate"):
93
+ return payload
94
+ retry = err.headers.get("Retry-After")
95
+ message = (payload.get("error") if isinstance(payload, dict)
96
+ else None) or "Moveezi API returned %s" % err.code
97
+ raise MoveeziError(message, status=err.code, body=payload,
98
+ rate_limit=self.last_rate_limit,
99
+ retry_after=int(retry) if retry else None) from None
100
+
101
+ # -- content ----------------------------------------------------------
102
+ def list_pages(self, limit: Optional[int] = None, cursor: Optional[str] = None,
103
+ prefix: Optional[str] = None) -> Dict[str, Any]:
104
+ """One page of the page index. Follow ``nextCursor`` for the rest."""
105
+ query = {k: v for k, v in (("limit", limit), ("cursor", cursor),
106
+ ("prefix", prefix)) if v is not None}
107
+ suffix = "?" + urllib.parse.urlencode(query) if query else ""
108
+ return self._request("/api/v1/pages" + suffix)
109
+
110
+ def all_pages(self, prefix: Optional[str] = None) -> Iterator[Dict[str, Any]]:
111
+ """Every page, following the cursor for you."""
112
+ cursor = None
113
+ while True:
114
+ batch = self.list_pages(limit=100, cursor=cursor, prefix=prefix)
115
+ for page in batch["pages"]:
116
+ yield page
117
+ cursor = batch.get("nextCursor")
118
+ if not cursor:
119
+ return
120
+
121
+ def get_page(self, slug: str, markdown: bool = False):
122
+ """One page. ``markdown=True`` returns the markdown document as a string."""
123
+ path = "/api/v1/pages/" + slug.strip("/")
124
+ return self._request(path, accept="text/markdown" if markdown else "application/json")
125
+
126
+ def get_pages(self, slugs: List[str]) -> Dict[str, Any]:
127
+ """Up to 50 pages in one round trip. Missing slugs come back marked."""
128
+ return self._request("/api/v1/pages/batch", "POST", {"slugs": list(slugs)})
129
+
130
+ def search(self, query: str, limit: Optional[int] = None) -> Dict[str, Any]:
131
+ """Ranked full-text search across the site."""
132
+ params = {"q": query}
133
+ if limit is not None:
134
+ params["limit"] = limit
135
+ return self._request("/api/v1/search?" + urllib.parse.urlencode(params))
136
+
137
+ # -- sales ------------------------------------------------------------
138
+ def validate_lead(self, lead: Dict[str, Any]) -> Dict[str, Any]:
139
+ """Check a lead against the same rules as ``submit_lead``, writing nothing."""
140
+ return self._request("/api/v1/lead/validate", "POST", lead)
141
+
142
+ def submit_lead(self, lead: Dict[str, Any],
143
+ idempotency_key: Optional[str] = None) -> Dict[str, Any]:
144
+ """Send an enquiry to the Moveezi sales team.
145
+
146
+ This creates a CRM record and emails a human, so only call it on behalf
147
+ of someone who asked you to. With an ``idempotency_key``, a retry
148
+ replays the first response instead of creating a second lead.
149
+ """
150
+ data = json.dumps(lead).encode("utf-8")
151
+ headers = {"Accept": "application/json", "Content-Type": "application/json",
152
+ "User-Agent": self.user_agent}
153
+ if idempotency_key:
154
+ headers["Idempotency-Key"] = idempotency_key
155
+ request = urllib.request.Request(self.base_url + "/api/lead", data=data,
156
+ headers=headers, method="POST")
157
+ try:
158
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
159
+ self.last_rate_limit = _rate_limit(response.headers)
160
+ return json.loads(response.read().decode("utf-8"))
161
+ except urllib.error.HTTPError as err:
162
+ raw = err.read().decode("utf-8", "replace")
163
+ try:
164
+ payload = json.loads(raw)
165
+ except ValueError:
166
+ payload = raw
167
+ retry = err.headers.get("Retry-After")
168
+ raise MoveeziError(
169
+ (payload.get("error") if isinstance(payload, dict) else None)
170
+ or "Moveezi API returned %s" % err.code,
171
+ status=err.code, body=payload, rate_limit=_rate_limit(err.headers),
172
+ retry_after=int(retry) if retry else None) from None
173
+
174
+ # -- documents --------------------------------------------------------
175
+ def openapi(self) -> Dict[str, Any]:
176
+ """The OpenAPI 3.1 description of everything above."""
177
+ return self._request("/openapi.json")
178
+
179
+ def sandbox(self) -> Dict[str, Any]:
180
+ """Where the sandbox is and what it covers."""
181
+ return self._request("/api/v1/sandbox")
182
+
183
+ def auth_doc(self) -> str:
184
+ """How authentication works here, which is: it does not."""
185
+ return self._request("/auth.md", accept="text/markdown")
186
+
187
+ def deprecation_policy(self) -> str:
188
+ """How this API is allowed to change, and how much notice you get."""
189
+ return self._request("/api-deprecation-policy", accept="text/markdown")
@@ -0,0 +1,115 @@
1
+ """`python -m moveezi` / the `moveezi` console script."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+
8
+ from . import MCP_ENDPOINT, Moveezi, MoveeziError, __version__
9
+
10
+ LEAD_FIELDS = ("name", "email", "company", "phone", "dialcode", "country",
11
+ "message", "intent", "segment", "warehouses", "moves")
12
+
13
+
14
+ def main(argv=None) -> int:
15
+ parser = argparse.ArgumentParser(
16
+ prog="moveezi",
17
+ description="Read moveezi.com from a terminal. Nothing here needs a key.")
18
+ parser.add_argument("--version", action="version", version=__version__)
19
+ parser.add_argument("--base", default=None, help="point at a different host")
20
+ parser.add_argument("--json", action="store_true", help="raw JSON instead of text")
21
+ sub = parser.add_subparsers(dest="command", required=True)
22
+
23
+ p = sub.add_parser("search", help="search moveezi.com")
24
+ p.add_argument("query", nargs="+")
25
+ p.add_argument("--limit", type=int)
26
+
27
+ p = sub.add_parser("page", help="one page as markdown")
28
+ p.add_argument("slug")
29
+
30
+ p = sub.add_parser("pages", help="every published page")
31
+ p.add_argument("--prefix")
32
+
33
+ p = sub.add_parser("lead", help="check a lead; --submit to actually send it")
34
+ for field in LEAD_FIELDS:
35
+ p.add_argument("--" + field)
36
+ p.add_argument("--idempotency-key")
37
+ p.add_argument("--submit", action="store_true")
38
+
39
+ sub.add_parser("openapi", help="the OpenAPI 3.1 document")
40
+ sub.add_parser("auth", help="how to authenticate here (you do not)")
41
+ sub.add_parser("policy", help="versioning and deprecation policy")
42
+ sub.add_parser("mcp", help="MCP endpoint and client config")
43
+
44
+ args = parser.parse_args(argv)
45
+ client = Moveezi(args.base) if args.base else Moveezi()
46
+
47
+ def out(value):
48
+ if args.json or not isinstance(value, str):
49
+ print(json.dumps(value, indent=2))
50
+ else:
51
+ print(value)
52
+
53
+ try:
54
+ if args.command == "search":
55
+ data = client.search(" ".join(args.query), args.limit)
56
+ if args.json:
57
+ return out(data) or 0
58
+ if not data["count"]:
59
+ print("Nothing on moveezi.com matches that.")
60
+ return 0
61
+ for r in data["results"]:
62
+ print("\n%s\n %s\n %s" % (r["title"], r["url"], r["snippet"]))
63
+ return 0
64
+ if args.command == "page":
65
+ out(client.get_page(args.slug, markdown=not args.json))
66
+ return 0
67
+ if args.command == "pages":
68
+ pages = list(client.all_pages(args.prefix))
69
+ if args.json:
70
+ return out(pages) or 0
71
+ for p in pages:
72
+ print("%-34s %s" % (p["slug"], p["title"]))
73
+ return 0
74
+ if args.command == "lead":
75
+ lead = {f: getattr(args, f) for f in LEAD_FIELDS if getattr(args, f)}
76
+ if args.submit:
77
+ out(client.submit_lead(lead, args.idempotency_key))
78
+ return 0
79
+ res = client.validate_lead(lead)
80
+ if args.json:
81
+ return out(res) or 0
82
+ if res["wouldSubmit"]:
83
+ print("Looks good. Nothing was sent - add --submit to send it.")
84
+ return 0
85
+ print("This would be rejected:")
86
+ for problem in res["problems"]:
87
+ print(" %s: %s" % (problem["field"], problem["error"]))
88
+ return 1
89
+ if args.command == "openapi":
90
+ return out(client.openapi()) or 0
91
+ if args.command == "auth":
92
+ return out(client.auth_doc()) or 0
93
+ if args.command == "policy":
94
+ return out(client.deprecation_policy()) or 0
95
+ if args.command == "mcp":
96
+ config = {"mcpServers": {"moveezi": {"type": "http", "url": MCP_ENDPOINT}}}
97
+ if args.json:
98
+ return out(config) or 0
99
+ print("Moveezi speaks MCP over Streamable HTTP.\n\n %s\n\n"
100
+ "Claude Code:\n claude mcp add --transport http moveezi %s\n\n"
101
+ "Any client that reads a config file:\n%s\n\n"
102
+ "No credentials. Tools: search_moveezi_site, get_moveezi_page,\n"
103
+ "list_moveezi_pages, get_moveezi_pricing."
104
+ % (MCP_ENDPOINT, MCP_ENDPOINT, json.dumps(config, indent=2)))
105
+ return 0
106
+ except MoveeziError as err:
107
+ print("moveezi: %s" % err, file=sys.stderr)
108
+ if err.retry_after:
109
+ print(" rate limited; retry in %ss" % err.retry_after, file=sys.stderr)
110
+ return 1
111
+ return 2
112
+
113
+
114
+ if __name__ == "__main__":
115
+ sys.exit(main())
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: moveezi
3
+ Version: 1.0.0
4
+ Summary: Client for the public Moveezi API: read moveezi.com as structured data, search it, and reach the sales team. No credentials needed.
5
+ Author-email: Dumont Pty Ltd <sales@moveezi.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://www.moveezi.com/developers
8
+ Project-URL: Documentation, https://www.moveezi.com/openapi.json
9
+ Project-URL: Repository, https://github.com/DumontAI/moveezi-sdk
10
+ Project-URL: Issues, https://github.com/DumontAI/moveezi-sdk/issues
11
+ Keywords: moveezi,moving,relocation,storage,logistics,api,sdk,mcp,agent,llm
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Office/Business
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Moveezi SDK
21
+
22
+ Client libraries and a CLI for the public [Moveezi](https://www.moveezi.com/) API.
23
+
24
+ [Moveezi](https://www.moveezi.com/) is the operating system for moving, relocation and
25
+ storage companies: sales and quoting, on-site and video surveys, dispatch and crew
26
+ scheduling, a mobile crew app, warehousing, sea/air/road shipment tracking, a live control
27
+ tower and per-job profit and loss.
28
+
29
+ **There is nothing to authenticate with.** No key, no signup, no token. Every read is open.
30
+ The one write, `submitLead`, is the same endpoint the website's own forms post to, and it
31
+ has a sandbox so you can check a payload without creating anything.
32
+
33
+ - API reference: <https://www.moveezi.com/openapi.json>
34
+ - Developer portal: <https://www.moveezi.com/developers>
35
+ - MCP server: `https://www.moveezi.com/mcp` (Streamable HTTP, no credentials)
36
+ - Authentication, in one page: <https://www.moveezi.com/auth.md>
37
+
38
+ ## Install
39
+
40
+ **JavaScript / TypeScript** (Node 18+, zero dependencies):
41
+
42
+ ```bash
43
+ npm install moveezi
44
+ # or straight from source, no registry account needed:
45
+ npm install DumontAI/moveezi-sdk
46
+ ```
47
+
48
+ **Python** (3.9+, standard library only):
49
+
50
+ ```bash
51
+ pip install moveezi
52
+ ```
53
+
54
+ **CLI**, without installing anything:
55
+
56
+ ```bash
57
+ npx moveezi search "video survey"
58
+ ```
59
+
60
+ ## Use it
61
+
62
+ ```js
63
+ import { MoveeziClient } from "moveezi";
64
+
65
+ const moveezi = new MoveeziClient();
66
+
67
+ const hits = await moveezi.searchSite("video survey", { limit: 3 });
68
+ const pricing = await moveezi.getPage("pricing", { format: "markdown" });
69
+ const pages = await moveezi.allPages({ prefix: "blog/" });
70
+
71
+ // Check a lead before you send one. Creates nothing.
72
+ const check = await moveezi.validateLead({ name: "Ada", email: "ada@example.com" });
73
+ if (check.wouldSubmit) {
74
+ await moveezi.submitLead(
75
+ { name: "Ada", email: "ada@example.com", intent: "demo" },
76
+ { idempotencyKey: crypto.randomUUID() }, // a retry will not create a second lead
77
+ );
78
+ }
79
+ ```
80
+
81
+ ```python
82
+ from moveezi import Moveezi
83
+
84
+ moveezi = Moveezi()
85
+ hits = moveezi.search("video survey", limit=3)
86
+ pricing = moveezi.get_page("pricing", markdown=True)
87
+ pages = list(moveezi.all_pages(prefix="blog/"))
88
+
89
+ check = moveezi.validate_lead({"name": "Ada", "email": "ada@example.com"})
90
+ if check["wouldSubmit"]:
91
+ moveezi.submit_lead({"name": "Ada", "email": "ada@example.com"},
92
+ idempotency_key="a3f1c9e2")
93
+ ```
94
+
95
+ ## CLI
96
+
97
+ ```
98
+ moveezi search <query> Search moveezi.com
99
+ moveezi page <slug> One page as markdown
100
+ moveezi pages [--prefix] Every published page
101
+ moveezi batch <slug...> Several pages in one request
102
+ moveezi pricing The pricing page
103
+ moveezi lead [--submit] Check a lead; only sends with --submit
104
+ moveezi openapi The OpenAPI 3.1 document
105
+ moveezi auth How to authenticate here (you do not)
106
+ moveezi policy Versioning and deprecation policy
107
+ moveezi mcp MCP endpoint and client config
108
+ ```
109
+
110
+ ## Rate limits
111
+
112
+ Reads are 120 requests per minute per IP. `POST /api/lead` is 5 per hour per IP. Both
113
+ report `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and `RateLimit-Policy`
114
+ on every response, and `Retry-After` on a 429. Both clients expose them:
115
+
116
+ ```js
117
+ await moveezi.listPages();
118
+ moveezi.lastRateLimit; // { limit: 120, remaining: 119, reset: 60 }
119
+ ```
120
+
121
+ A 429 throws a `MoveeziError` with `retryAfter` in seconds, so you can back off on real
122
+ numbers rather than guessing.
123
+
124
+ ## If you are an AI agent
125
+
126
+ You may not need this package at all. The API is plain HTTP with an
127
+ [OpenAPI 3.1 document](https://www.moveezi.com/openapi.json), every page is available as
128
+ markdown (append `.md`, or send `Accept: text/markdown`), and there is an
129
+ [MCP server](https://www.moveezi.com/mcp) with tools for search, page retrieval and
130
+ pricing. Start at [llms.txt](https://www.moveezi.com/llms.txt).
131
+
132
+ Please only call `submitLead` on behalf of a person who asked you to contact Moveezi, and
133
+ send an `Idempotency-Key` so a retry does not create a duplicate. `validateLead` is there
134
+ so you never have to learn the rules by creating a bad record.
135
+
136
+ ## Stability
137
+
138
+ The version is in the path (`/api/v1`) and this API changes additively within it. Anything
139
+ being withdrawn gets `Deprecation` and `Sunset` headers and six months' notice first. Full
140
+ policy: <https://www.moveezi.com/api-deprecation-policy>.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ npm test # JavaScript, no network
146
+ MOVEEZI_LIVE=1 npm test # plus a couple against production
147
+ cd python && python3 -m unittest test_moveezi
148
+ ```
149
+
150
+ ## Licence
151
+
152
+ MIT. See [LICENSE](LICENSE). The API and the content it serves belong to Dumont Pty Ltd.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ moveezi/__init__.py
4
+ moveezi/__main__.py
5
+ moveezi.egg-info/PKG-INFO
6
+ moveezi.egg-info/SOURCES.txt
7
+ moveezi.egg-info/dependency_links.txt
8
+ moveezi.egg-info/entry_points.txt
9
+ moveezi.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ moveezi = moveezi.__main__:main
@@ -0,0 +1 @@
1
+ moveezi
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "moveezi"
7
+ version = "1.0.0"
8
+ description = "Client for the public Moveezi API: read moveezi.com as structured data, search it, and reach the sales team. No credentials needed."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{ name = "Dumont Pty Ltd", email = "sales@moveezi.com" }]
13
+ keywords = ["moveezi", "moving", "relocation", "storage", "logistics", "api", "sdk", "mcp", "agent", "llm"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Office/Business",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Homepage = "https://www.moveezi.com/developers"
25
+ Documentation = "https://www.moveezi.com/openapi.json"
26
+ Repository = "https://github.com/DumontAI/moveezi-sdk"
27
+ Issues = "https://github.com/DumontAI/moveezi-sdk/issues"
28
+
29
+ [project.scripts]
30
+ moveezi = "moveezi.__main__:main"
31
+
32
+ [tool.setuptools.packages.find]
33
+ include = ["moveezi*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+