Devnors Data Python SDK: One POST Gateway to Chinese Legal, Enterprise and Content Data
Devnors Data - Python SDK: 接口,数据API,裁判文书,Legal Case,法律法规,法条,Law Article,企业工商,Company Registry,企业年报,Annual Report,税号开票,Tax Invoice,失信核查,Dishonest Judgment Debtor,被执行人,Enforcement Debtor,关键词指数,Keyword Index,微信指数,WeChat Index,热搜榜,Hot Rank,微博热搜,抖音热搜,快递查询,Express Tracking,快递公司编号,统一查询,/v1/data/query,API Key,MCP,AI Agent
At a glance
- What is it?
- This SDK wraps a hosted data service behind a single POST /v1/data/query call, with typed helpers for legal cases, company registry records, hot-rank lists and express tracking. The useful part is the uniform envelope and the retry and pagination behaviour; the catch is that the data itself lives on Devnors servers, not in the package.
- Who is it for?
- Adopt this SDK if you are building Chinese-language legal, enterprise-registry or content-index lookups into a Python service and want the transport layer handled: retries, pagination and typed error fields are already there. Do not adopt it if you need the data to live inside your own perimeter, if you need a bulk export rather than query-shaped access, or if you cannot accept that every call is metered in units and can fail with code=insufficient_balance at status 402.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem: many Chinese data sources, one query shape
Anyone who has wired Chinese legal or corporate data into an application knows the shape of the problem. Judgment documents come from one kind of interface, company registration from another, hot-rank lists from a third, and each has its own pagination convention, its own error vocabulary and its own identifier scheme. Devnors Data takes the position that these should all be reachable through one endpoint, POST /v1/data/query, parameterised by a domain and a type. The Python SDK is the client for that endpoint.
The intended audience is narrow but real. The README frames every hit as designed for AI agents, and the repository lists a companion MCP server for Claude, Cursor and WorkBuddy. So the primary user is someone building a retrieval step for an agent or an internal tool, where the query is composed at runtime and the answer is fed onward rather than read by a person. The secondary user is a Python developer who needs one of these datasets occasionally and does not want to learn four separate APIs to get it. If you need bulk analytical access to the same corpora, this is not that product.
How the gateway is structured: domain, type, query
The architecture visible in the README is a thin client over a hosted service. There is no local index, no embedded database and no crawler. The client holds an API key, builds a request against a base URL, and returns whatever the server sends back.
The addressing scheme is a two-level pair. A domain is the broad area (legal, content, enterprise, cloud, research) and a type is the specific dataset within it. The README's table lists these pairs and marks each as live. Legal offers case, law_article and law_catalog. Content offers keyword_index, suggest_list, keyword_word, wechat_index_v2 and hot_rank. Enterprise is by far the largest group, running from company_detail_v2 and annual_report_list through taxpayer_basic, tax_credit_level, admin_punishment, court_notice_list, exec_person and breach_of_trust up to listed_company and listed_company_neeq. Cloud covers express, express_com, web_search and invoice_ocr. Research covers paper_search, patent_search, journal_search and their detail counterparts, plus scholar_search and scholar_detail.
The convenience methods are not separate transports. client.legal_cases(query, top_k=10) and client.legal_laws(query) are named wrappers that resolve to the same gateway, which is why client.query(domain="legal", type="case", query="劳动争议", top_k=5) exists as the general form. That matters for planning: any type in the table is reachable even if no helper method is documented for it, because the helper surface is a convenience layer over a uniform call.
The response envelope appears to be consistent. The README shows res["hits"] for result rows, res["total_hits"] for the count that pagination depends on, and res["units"] for tokens charged. It also mentions request_id on successful calls. That uniformity is the actual product here. The datasets are the payload; the envelope is what saves you from writing five adapters.
Getting it running: install, key, first call
Installation is a single command from the README:
pip install devnors-data
The key comes from the developer console at data.devnors.com/console, and the README shows it passed directly as DevnorsData(api_key="devnors_sk_live_xxx") or supplied through the environment variable DEVNORS_API_KEY. There is a third configuration point, DEVNORS_DATA_BASE_URL, described as an optional private or self-hosted base URL, with https://data.devnors.com as the documented value. That variable is worth noting because it is the only documented escape hatch if you are pointing at a different deployment.
A synchronous first call looks like this:
from devnors_data import DevnorsData client = DevnorsData(api_key="devnors_sk_live_xxx") res = client.legal_cases("民间借贷 利息", top_k=10)
The asynchronous path is a separate class, AsyncDevnorsData, used with await and asyncio.run, shown in the README with await client.legal_cases("交通事故 责任认定"). Two classes rather than one dual-mode client is a deliberate split: you pick your concurrency model at import time and there is no ambiguity about whether a call returns a coroutine.
Pagination is handled by a generator, not by a manual loop over offsets. The README's example is client.paginate("legal", "case", "借贷纠纷", page_size=20, max_items=100), which walks pages using total_hits and stops at max_items. Note the argument order: domain, type, query. It matches the general query method, not the named helper.
Retry policy and the error fields you actually branch on
The retry rules are stated precisely enough to reason about. Automatic retry applies to 429, 5xx and connection errors, using bounded exponential backoff. It explicitly does not apply to 400, 401, 402 and 501. You can turn it off entirely with DevnorsData(..., max_retries=0).
The choice of 402 as non-retryable is the interesting one. A 402 here means insufficient balance, surfaced as DevnorsDataError(code="insufficient_balance", status=402). Retrying a payment failure would just burn time, so excluding it is correct, but it also means an unattended agent can hit a hard stop that no amount of backoff will clear. Any production wrapper needs to treat insufficient_balance as a terminal condition with its own alerting path, distinct from the transient failures the client already absorbs.
Rate limiting arrives as code="rate_limited" at 429, which the client will retry. Each DevnorsDataError carries code, retryable, next_action and request_id. The presence of a next_action field is a small but meaningful design decision: the server is telling the caller what to do rather than leaving the client to infer it from the status code. Branching on code and retryable is more stable than branching on HTTP status, since several codes can share a status.
Billing visibility is per call. Successful responses include units and request_id. Since units are described as tokens charged, cost scales with how much you ask for, which makes top_k and page_size the two knobs that control spend. The README does not publish a price per unit, so the only way to know your cost profile is to observe units on real calls before you commit to a query volume.
Where the SDK stops: hosted dependency, coverage gaps and bulk work
The first limitation is structural rather than a bug. The package contains no data. Every lookup is a network call to a service you do not operate, metered in units, gated by an API key. If your requirement is that Chinese legal or corporate records sit inside your own network boundary, this SDK cannot satisfy it. DEVNORS_DATA_BASE_URL suggests a private deployment is possible, but the README does not describe how one is provisioned or licensed, so treat that as an open question rather than a supported path.
The second limitation is that the type table tells you what exists, not what your account can reach. Every row is marked live, but entitlement is a separate matter that the README does not address. A type being listed is not a promise that your key returns data for it.
The third is that the access pattern is query-shaped, not export-shaped. There is a paginate helper with a max_items ceiling, which is fine for feeding a hundred judgment documents to a model and wrong for building a local corpus. If your work is statistical analysis over a large slice of the enterprise dataset, you will be issuing an enormous number of metered calls through a retry-and-backoff client, which is the least efficient way to move that much data.
The fourth is that the README gives no coverage statement for any individual dataset. It does not say which courts are represented in legal cases, how current the annual report data is, or how far back the hot-rank history goes. For agent retrieval over recent material this may not matter. For anything where completeness is the point, it is the first thing to establish, and the README will not establish it for you.
Compared with calling each source directly
The obvious alternative is to integrate the upstream providers yourself: a court document service for judgments, a corporate registry data vendor for company records, a social platform endpoint for hot ranks, a courier aggregator for tracking. That approach gives you direct control over contracts, coverage guarantees and data residency, and it removes the per-call units layer in favour of whatever those vendors charge.
The difference in approach is where the work sits. Direct integration means you own the normalisation: four response schemas, four error taxonomies, four pagination conventions, four retry policies, and the maintenance when any of them changes. This SDK collapses that into one envelope with hits, total_hits, units and request_id, one error type with code, retryable, next_action and request_id, and one paginate generator. You are trading control and, potentially, per-record cost for a single integration surface and a single place to look when something breaks.
There is a second alternative worth naming: the companion devnors-data-mcp repository. If your consumer is an agent host that speaks MCP rather than a Python process, the MCP server is the more direct route to the same gateway, and the Python SDK is the wrong layer. The two are not competing implementations; they are two front doors to one backend, and picking the wrong one means writing glue you did not need.
Maintenance, licence and what the package commits you to
The licence is MIT, which covers the client code in this repository. It does not extend to the data service behind the endpoint, and the README draws no distinction between the two. The practical reading is that you may fork, modify and vendor the client freely, while your access to the gateway remains governed by your console account and whatever terms attach to it. That is a question for the service terms, not the repository licence, and it is not legal advice to say so.
Upgrade cost is low on the client side and invisible on the server side. The package is a thin wrapper with no local state, so a version bump is unlikely to require migration work in your code. The real change surface is the domain and type table: when a type is renamed or retired, your query(domain=..., type=...) call is what breaks, and the client cannot shield you from that. Because the general query method takes domain and type as plain strings, there is no compile-time or type-checker protection against a stale type name. Centralising those strings in one module in your own codebase is the cheap defence.
The repository shows no retrieved releases, so there is no changelog to review before upgrading. Pin the version you install and read the diff when you move.
Editorial conclusion
Adopt this SDK if you are building Chinese-language legal, enterprise-registry or content-index lookups into a Python service and want the transport layer handled: retries, pagination and typed error fields are already there. Do not adopt it if you need the data to live inside your own perimeter, if you need a bulk export rather than query-shaped access, or if you cannot accept that every call is metered in units and can fail with code=insufficient_balance at status 402. Before writing production code, verify three things against the live console: which of the listed enterprise types your account can actually reach, what a representative query costs in units for your expected top_k, and whether DEVNORS_DATA_BASE_URL is needed for a private deployment. The package is MIT, so the client code is yours to fork; the data endpoint behind it is not.
Community notes