lambda-erp 0.6.11__tar.gz → 0.6.13__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.
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/PKG-INFO +2 -2
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/README.md +1 -1
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/main.py +2 -1
- lambda_erp-0.6.13/api/routers/mcp.py +164 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/pyproject.toml +1 -1
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/.gitignore +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/LICENSE +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/__init__.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/attachments.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/auth.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/bootstrap.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/chat.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/demo_limits.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/deps.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/errors.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/oauth.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/pdf.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/providers.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/remarks_md.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/__init__.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/accounting.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/admin.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/analytics.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/documents.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/masters.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/proposals.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/reports.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/setup.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/services.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/templates/document.html +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/templates/proposal.html +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/docs/agents/README.md +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/frontend/LICENSE +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/frontend/README.md +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/database.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/model.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.6.11 → lambda_erp-0.6.13}/terraform/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lambda-erp
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.13
|
|
4
4
|
Summary: Core ERP logic - accounting, sales, purchasing, inventory
|
|
5
5
|
Author: TORUS INVESTMENTS AG
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -161,12 +161,12 @@ The result is booked in one step — chart of accounts, sensible default account
|
|
|
161
161
|
- Server-side aggregation tool for in-chat factual answers across large datasets
|
|
162
162
|
- PDF / image attachment → add invoices, create quotations, etc. all directly by adding them in the chat
|
|
163
163
|
- **Programmatic API** — drive the ERP from external systems with per-user, role-capped Bearer keys: a [chat API](docs/chat-api.md) (converse with the agent) and a [REST API](docs/rest-api.md) (documents, masters, reports — the same endpoints the web app uses, so a connector or sync job can create and submit documents directly). Both off by default; an admin enables each independently and issues keys.
|
|
164
|
+
- **MCP server** — the ERP speaks the [Model Context Protocol](https://modelcontextprotocol.io) at `POST /api/mcp`, so **any MCP-capable agent (Claude, Codex, an IDE assistant) can drive it directly** — list/create/update/submit documents, manage master data, run reports — as *structured, validated tool calls*, not brittle screen-scraping or hand-written HTTP glue. The tool list is built from the live registries, so a plugin's own doctypes (e.g. a CRM's leads/contacts) are exposed automatically. It reuses the *same* Bearer keys and roles as the REST API — a viewer key is read-only, a manager key can write — so access is scoped, auditable, and off by default. In practice: point your assistant at your instance and ask "which invoices are overdue?" or "create a quotation for Acme with 3× SVC-SPARK" and it operates on real data, from wherever you already work. The API-keys settings page hands you ready-to-paste Claude and Codex config the moment you create a key.
|
|
164
165
|
- Auth with admin/manager/viewer roles plus a public demo mode
|
|
165
166
|
- Full test suite that exercises every cycle against an in-memory SQLite
|
|
166
167
|
|
|
167
168
|
## What's still todo (Suggestions welcome)
|
|
168
169
|
|
|
169
|
-
- MCP integration for supplier/customer communication (quotes, orders, confirmations)
|
|
170
170
|
- Multi-currency beyond the simplified current handling
|
|
171
171
|
- Workflows / approval chains
|
|
172
172
|
- Serial & batch tracking
|
|
@@ -134,12 +134,12 @@ The result is booked in one step — chart of accounts, sensible default account
|
|
|
134
134
|
- Server-side aggregation tool for in-chat factual answers across large datasets
|
|
135
135
|
- PDF / image attachment → add invoices, create quotations, etc. all directly by adding them in the chat
|
|
136
136
|
- **Programmatic API** — drive the ERP from external systems with per-user, role-capped Bearer keys: a [chat API](docs/chat-api.md) (converse with the agent) and a [REST API](docs/rest-api.md) (documents, masters, reports — the same endpoints the web app uses, so a connector or sync job can create and submit documents directly). Both off by default; an admin enables each independently and issues keys.
|
|
137
|
+
- **MCP server** — the ERP speaks the [Model Context Protocol](https://modelcontextprotocol.io) at `POST /api/mcp`, so **any MCP-capable agent (Claude, Codex, an IDE assistant) can drive it directly** — list/create/update/submit documents, manage master data, run reports — as *structured, validated tool calls*, not brittle screen-scraping or hand-written HTTP glue. The tool list is built from the live registries, so a plugin's own doctypes (e.g. a CRM's leads/contacts) are exposed automatically. It reuses the *same* Bearer keys and roles as the REST API — a viewer key is read-only, a manager key can write — so access is scoped, auditable, and off by default. In practice: point your assistant at your instance and ask "which invoices are overdue?" or "create a quotation for Acme with 3× SVC-SPARK" and it operates on real data, from wherever you already work. The API-keys settings page hands you ready-to-paste Claude and Codex config the moment you create a key.
|
|
137
138
|
- Auth with admin/manager/viewer roles plus a public demo mode
|
|
138
139
|
- Full test suite that exercises every cycle against an in-memory SQLite
|
|
139
140
|
|
|
140
141
|
## What's still todo (Suggestions welcome)
|
|
141
142
|
|
|
142
|
-
- MCP integration for supplier/customer communication (quotes, orders, confirmations)
|
|
143
143
|
- Multi-currency beyond the simplified current handling
|
|
144
144
|
- Workflows / approval chains
|
|
145
145
|
- Serial & batch tracking
|
|
@@ -19,7 +19,7 @@ from api.auth import router as auth_router, COOKIE_NAME, decode_token
|
|
|
19
19
|
from api.oauth import router as oauth_router
|
|
20
20
|
from api.attachments import router as attachments_router
|
|
21
21
|
from api.chat import chat_websocket, router as chat_router
|
|
22
|
-
from api.routers import admin, documents, masters, reports, setup as setup_router, bank_reconciliation, analytics, accounting, proposals, chat_api
|
|
22
|
+
from api.routers import admin, documents, masters, reports, setup as setup_router, bank_reconciliation, analytics, accounting, proposals, chat_api, mcp
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def load_plugins() -> None:
|
|
@@ -114,6 +114,7 @@ app.include_router(accounting.router, prefix="/api")
|
|
|
114
114
|
app.include_router(admin.router, prefix="/api")
|
|
115
115
|
app.include_router(chat_router, prefix="/api")
|
|
116
116
|
app.include_router(chat_api.router, prefix="/api")
|
|
117
|
+
app.include_router(mcp.router, prefix="/api")
|
|
117
118
|
|
|
118
119
|
|
|
119
120
|
@app.get("/api/health")
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) endpoint — the fine-grained ERP tool surface for
|
|
2
|
+
LLM agents (Claude, Codex, …) over MCP's Streamable HTTP transport.
|
|
3
|
+
|
|
4
|
+
Reuses everything the chat already has, so there's almost no new logic:
|
|
5
|
+
* schemas — build_tools() (the live, plugin-widened tool list).
|
|
6
|
+
* execution — the same TOOL_HANDLERS (which run validate()).
|
|
7
|
+
* auth — get_current_user: a Bearer API key acts AS its user at the key's
|
|
8
|
+
role, exactly like the REST API, gated by the same
|
|
9
|
+
`rest_api_enabled` Settings flag. No separate MCP credential.
|
|
10
|
+
|
|
11
|
+
Because the tool schemas come from the live registries, a plugin that registers
|
|
12
|
+
doctypes/masters (e.g. the internal CRM's lead/contact/activity) is exposed here
|
|
13
|
+
automatically — the MCP surface is modular by construction.
|
|
14
|
+
|
|
15
|
+
Transport: a single POST /api/mcp speaking JSON-RPC 2.0 (request → JSON result;
|
|
16
|
+
notifications → 202, no body). That's the minimum a tool-only server needs.
|
|
17
|
+
"""
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
21
|
+
from fastapi.responses import JSONResponse, Response
|
|
22
|
+
|
|
23
|
+
from lambda_erp import get_app_version
|
|
24
|
+
from api.auth import get_current_user
|
|
25
|
+
from api import chat as chat_mod
|
|
26
|
+
from api.chat import TOOL_HANDLERS, build_tools
|
|
27
|
+
|
|
28
|
+
router = APIRouter(tags=["mcp"])
|
|
29
|
+
|
|
30
|
+
PROTOCOL_VERSION = "2025-06-18"
|
|
31
|
+
|
|
32
|
+
# Chat-session-only tools have no meaning without a chat session — keep them out
|
|
33
|
+
# of the MCP surface (an MCP client owns its own context).
|
|
34
|
+
_EXCLUDE = {
|
|
35
|
+
"retrieve_chat_history",
|
|
36
|
+
"list_chat_attachments",
|
|
37
|
+
"retrieve_chat_attachment",
|
|
38
|
+
"create_custom_analytics_report",
|
|
39
|
+
"get_custom_analytics_report",
|
|
40
|
+
"update_custom_analytics_report",
|
|
41
|
+
"plan_company_setup",
|
|
42
|
+
"apply_company_setup",
|
|
43
|
+
}
|
|
44
|
+
# Mirror the REST permission model: reads are viewer+, writes are manager+,
|
|
45
|
+
# delete_master is admin-only (the handler also re-checks).
|
|
46
|
+
_WRITE = {
|
|
47
|
+
"create_document", "update_document", "submit_document", "cancel_document",
|
|
48
|
+
"discard_document", "convert_document", "create_master", "update_master",
|
|
49
|
+
}
|
|
50
|
+
_ADMIN = {"delete_master"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _can_write(role) -> bool:
|
|
54
|
+
return role in ("manager", "admin", "public_manager")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _allowed(name: str, role) -> bool:
|
|
58
|
+
if name in _EXCLUDE:
|
|
59
|
+
return False
|
|
60
|
+
if name in _ADMIN:
|
|
61
|
+
return role == "admin"
|
|
62
|
+
if name in _WRITE:
|
|
63
|
+
return _can_write(role)
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _require_caller(request: Request) -> dict:
|
|
68
|
+
"""A valid Bearer API key is mandatory for MCP — no cookie/public fallback.
|
|
69
|
+
get_current_user then validates the key and applies `rest_api_enabled`."""
|
|
70
|
+
auth = request.headers.get("authorization", "")
|
|
71
|
+
if not auth.lower().startswith("bearer "):
|
|
72
|
+
raise HTTPException(status_code=401, detail="MCP requires a Bearer API key")
|
|
73
|
+
return get_current_user(request)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _tools(role) -> list:
|
|
77
|
+
out = []
|
|
78
|
+
for tool in build_tools():
|
|
79
|
+
fn = tool["function"]
|
|
80
|
+
if not _allowed(fn["name"], role):
|
|
81
|
+
continue
|
|
82
|
+
out.append({
|
|
83
|
+
"name": fn["name"],
|
|
84
|
+
"description": fn.get("description", ""),
|
|
85
|
+
"inputSchema": fn.get("parameters") or {"type": "object", "properties": {}},
|
|
86
|
+
})
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _call(name: str, args: dict, user: dict):
|
|
91
|
+
role = user.get("role")
|
|
92
|
+
if not _allowed(name, role):
|
|
93
|
+
return {"error": f"'{name}' is not available to a {role or 'viewer'} key."}
|
|
94
|
+
handlers = dict(TOOL_HANDLERS)
|
|
95
|
+
# delete_master needs the caller's role (admin-only); handled by the chat's
|
|
96
|
+
# role-aware variant.
|
|
97
|
+
handlers["delete_master"] = lambda a: chat_mod._handle_delete_master(a, user)
|
|
98
|
+
handler = handlers.get(name)
|
|
99
|
+
if handler is None:
|
|
100
|
+
raise KeyError(name)
|
|
101
|
+
return handler(args or {})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _rpc_error(mid, code: int, message: str) -> dict:
|
|
105
|
+
return {"jsonrpc": "2.0", "id": mid, "error": {"code": code, "message": message}}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _handle(msg: dict, user: dict):
|
|
109
|
+
"""Handle one JSON-RPC message. Returns a response dict, or None for a
|
|
110
|
+
notification (no `id`)."""
|
|
111
|
+
method = msg.get("method")
|
|
112
|
+
mid = msg.get("id")
|
|
113
|
+
is_notification = "id" not in msg
|
|
114
|
+
|
|
115
|
+
def result(payload):
|
|
116
|
+
return None if is_notification else {"jsonrpc": "2.0", "id": mid, "result": payload}
|
|
117
|
+
|
|
118
|
+
if method == "initialize":
|
|
119
|
+
return result({
|
|
120
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
121
|
+
"capabilities": {"tools": {"listChanged": False}},
|
|
122
|
+
"serverInfo": {"name": "lambda-erp", "version": get_app_version()},
|
|
123
|
+
})
|
|
124
|
+
if method in ("notifications/initialized", "notifications/cancelled"):
|
|
125
|
+
return None
|
|
126
|
+
if method == "ping":
|
|
127
|
+
return result({})
|
|
128
|
+
if method == "tools/list":
|
|
129
|
+
return result({"tools": _tools(user.get("role"))})
|
|
130
|
+
if method == "tools/call":
|
|
131
|
+
params = msg.get("params") or {}
|
|
132
|
+
name = params.get("name")
|
|
133
|
+
try:
|
|
134
|
+
out = _call(name, params.get("arguments") or {}, user)
|
|
135
|
+
except KeyError:
|
|
136
|
+
return _rpc_error(mid, -32602, f"Unknown tool: {name}")
|
|
137
|
+
except Exception as e: # noqa: BLE001 — surface as an MCP tool error, not a 500
|
|
138
|
+
out = {"error": str(e)}
|
|
139
|
+
is_error = isinstance(out, dict) and "error" in out
|
|
140
|
+
return result({
|
|
141
|
+
"content": [{"type": "text", "text": json.dumps(out, default=str, ensure_ascii=False)}],
|
|
142
|
+
"isError": is_error,
|
|
143
|
+
})
|
|
144
|
+
if is_notification:
|
|
145
|
+
return None
|
|
146
|
+
return _rpc_error(mid, -32601, f"Method not found: {method}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@router.post("/mcp")
|
|
150
|
+
async def mcp_endpoint(request: Request):
|
|
151
|
+
user = _require_caller(request)
|
|
152
|
+
try:
|
|
153
|
+
body = await request.json()
|
|
154
|
+
except Exception:
|
|
155
|
+
return JSONResponse(_rpc_error(None, -32700, "Parse error"), status_code=400)
|
|
156
|
+
|
|
157
|
+
# JSON-RPC batch (a list) or a single message.
|
|
158
|
+
if isinstance(body, list):
|
|
159
|
+
responses = [r for r in (_handle(m, user) for m in body) if r is not None]
|
|
160
|
+
return JSONResponse(responses) if responses else Response(status_code=202)
|
|
161
|
+
resp = _handle(body, user)
|
|
162
|
+
if resp is None:
|
|
163
|
+
return Response(status_code=202)
|
|
164
|
+
return JSONResponse(resp)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|