fix(recipes): normalize newlines + add update_recipe (v0.6.1)
Bug fix: literal backslash-n sequences in ingredients/instructions are now converted to real newline characters before sending to the API, so the server correctly splits ingredient lines into ingredientsList[]. New tool: update_recipe — partial update via mprecipeput with recipe.metaId; fetches current recipe in the same session to verify can_update and supply name fallback. Verified: recipe.metaId triggers update (not create). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ und wird in Claude Desktop eingebunden.
|
|||||||
|
|
||||||
## Aktueller Stand
|
## Aktueller Stand
|
||||||
|
|
||||||
### Implementierte Tools (v0.6.0)
|
### Implementierte Tools (v0.6.1)
|
||||||
|
|
||||||
| Kategorie | Tools |
|
| Kategorie | Tools |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -33,7 +33,7 @@ und wird in Claude Desktop eingebunden.
|
|||||||
| Listen | `create_list`, `delete_list` |
|
| Listen | `create_list`, `delete_list` |
|
||||||
| Kategorien | `create_category`, `delete_category` |
|
| Kategorien | `create_category`, `delete_category` |
|
||||||
| Aktivitäten | `like_post` |
|
| Aktivitäten | `like_post` |
|
||||||
| Rezepte | `get_recipes`, `get_recipe`, `create_recipe`, `delete_recipe` |
|
| Rezepte | `get_recipes`, `get_recipe`, `create_recipe`, `update_recipe`, `delete_recipe` |
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ und wird in Claude Desktop eingebunden.
|
|||||||
- v0.5.x: Listen-Management (create_list, delete_list) ✓
|
- v0.5.x: Listen-Management (create_list, delete_list) ✓
|
||||||
- v0.5.1: emoji + color in get_lists / create_list ✓
|
- v0.5.1: emoji + color in get_lists / create_list ✓
|
||||||
- v0.5.2: Mengenkonvention im create_task Docstring ✓
|
- v0.5.2: Mengenkonvention im create_task Docstring ✓
|
||||||
- v0.6.0: Rezept-Box (get_recipes, get_recipe, create_recipe, delete_recipe) ✓ ← aktuell
|
- v0.6.0: Rezept-Box (get_recipes, get_recipe, create_recipe, delete_recipe) ✓
|
||||||
- v0.6.1: update_recipe (Rezept bearbeiten)
|
- v0.6.1: update_recipe + Bugfix Zeilenumbrüche in create_recipe ✓ ← aktuell
|
||||||
- v0.6.2: mpadditemtolist (Zutaten → Einkaufsliste)
|
- v0.6.2: mpadditemtolist (Zutaten → Einkaufsliste)
|
||||||
- v0.5.3: update_list (Umbenennen, emoji/color ändern), Sharing-Verwaltung
|
- v0.5.3: update_list (Umbenennen, emoji/color ändern), Sharing-Verwaltung
|
||||||
- v0.7.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich)
|
- v0.7.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich)
|
||||||
@@ -133,6 +133,7 @@ Fehler bei falschen Parametern kommen nicht immer auf Top-Level:
|
|||||||
| `taskcreatelist` | `name`, `taskListType`, `sharedToAll`, `color`, `emoji` | `taskListType`: `"SHOPPING_LIST"`/`"TODOS"` |
|
| `taskcreatelist` | `name`, `taskListType`, `sharedToAll`, `color`, `emoji` | `taskListType`: `"SHOPPING_LIST"`/`"TODOS"` |
|
||||||
| `taskdeletelist` | `id` | metaId der Liste |
|
| `taskdeletelist` | `id` | metaId der Liste |
|
||||||
| `mprecipeput` | `recipe.name`, `recipe.isRecipe="true"`, `recipe.description`, `recipe.ingredients`, `recipe.instructions`, `recipe.prepTime`, `recipe.cookTime`, `recipe.serves`, `recipe.url` | Alle mit `recipe.`-Prefix! |
|
| `mprecipeput` | `recipe.name`, `recipe.isRecipe="true"`, `recipe.description`, `recipe.ingredients`, `recipe.instructions`, `recipe.prepTime`, `recipe.cookTime`, `recipe.serves`, `recipe.url` | Alle mit `recipe.`-Prefix! |
|
||||||
|
| `mprecipeput` (Update) | zusätzlich `recipe.metaId` | Vorhandene ID → Update statt Create |
|
||||||
| `metasync` (Rezepte lesen) | `id="recipe"` | liefert `a00.r.r.updatedCreated[]` |
|
| `metasync` (Rezepte lesen) | `id="recipe"` | liefert `a00.r.r.updatedCreated[]` |
|
||||||
|
|
||||||
### Self-Like-Restriction
|
### Self-Like-Restriction
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, tasks, and recipes directly from Claude.
|
MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, tasks, and recipes directly from Claude.
|
||||||
|
|
||||||
## Features (v0.6.0)
|
## Features (v0.6.1)
|
||||||
|
|
||||||
### Read
|
### Read
|
||||||
|
|
||||||
@@ -26,7 +26,8 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your
|
|||||||
- `create_category` -- create a custom category for a shopping list (with optional icon)
|
- `create_category` -- create a custom category for a shopping list (with optional icon)
|
||||||
- `delete_category` -- delete a custom category (system categories are protected)
|
- `delete_category` -- delete a custom category (system categories are protected)
|
||||||
- `like_post` -- like a wall post/activity
|
- `like_post` -- like a wall post/activity
|
||||||
- `create_recipe` -- create a new recipe (name, description, ingredients, instructions, prep/cook time, serves, url)
|
- `create_recipe` -- create a new recipe (name, description, ingredients, instructions, prep/cook time, serves, url); use `\n` to separate ingredient lines
|
||||||
|
- `update_recipe` -- update any field of an existing recipe (partial update — omitted fields unchanged)
|
||||||
- `delete_recipe` -- permanently delete a recipe (only own recipes)
|
- `delete_recipe` -- permanently delete a recipe (only own recipes)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|||||||
@@ -432,6 +432,26 @@ a00.r.r → vollständiges Rezept-Objekt
|
|||||||
|
|
||||||
**Verifiziert am:** 2026-04-16 via FW_DEBUG=1
|
**Verifiziert am:** 2026-04-16 via FW_DEBUG=1
|
||||||
|
|
||||||
|
### `mprecipeput` – Rezept aktualisieren (Update)
|
||||||
|
POST https://api.familywall.com/api/mprecipeput
|
||||||
|
|
||||||
|
Identisch zum Create-Aufruf, aber mit zusätzlichem `recipe.metaId`-Parameter.
|
||||||
|
Der Server unterscheidet Create vs. Update anhand ob `recipe.metaId` vorhanden ist.
|
||||||
|
Nur geänderte Felder müssen mitgeschickt werden (Partial Update).
|
||||||
|
`recipe.name` und `recipe.isRecipe="true"` sollten immer mitgeschickt werden.
|
||||||
|
|
||||||
|
**Zusätzlicher Parameter:**
|
||||||
|
|
||||||
|
| Parameter | Pflicht | Wert |
|
||||||
|
|---|---|---|
|
||||||
|
| `recipe.metaId` | ja (für Update) | metaId des zu aktualisierenden Rezepts |
|
||||||
|
|
||||||
|
**Newline-Hinweis:** Zutaten und Anleitung müssen echte `\n`-Zeichen enthalten
|
||||||
|
(nicht die zwei-Zeichen-Sequenz `\n`). Literale Backslash-n werden vom Server
|
||||||
|
als ein Element interpretiert → kein Splitting in `ingredientsList`.
|
||||||
|
|
||||||
|
**Verifiziert am:** 2026-04-16 via FW_DEBUG=1
|
||||||
|
|
||||||
### `metasync` (id='recipe') – Alle Rezepte abrufen
|
### `metasync` (id='recipe') – Alle Rezepte abrufen
|
||||||
POST https://api.familywall.com/api/metasync
|
POST https://api.familywall.com/api/metasync
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mcp-familywall"
|
name = "mcp-familywall"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
description = "MCP server for Family Wall — read your family's lists and tasks via Claude"
|
description = "MCP server for Family Wall — read your family's lists and tasks via Claude"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.6.0"
|
__version__ = "0.6.1"
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
|
|
||||||
Verified endpoints (2026-04-16 via FW_DEBUG=1):
|
Verified endpoints (2026-04-16 via FW_DEBUG=1):
|
||||||
- Create: POST mprecipeput — params use 'recipe.' prefix (recipe.name, etc.)
|
- Create: POST mprecipeput — params use 'recipe.' prefix (recipe.name, etc.)
|
||||||
|
- Update: POST mprecipeput with recipe.metaId — same prefix convention
|
||||||
- Read all: POST metasync with id='recipe' — response at a00.r.r.updatedCreated[]
|
- Read all: POST metasync with id='recipe' — response at a00.r.r.updatedCreated[]
|
||||||
- Delete: POST metadelete with id=<recipe_metaId>
|
- Delete: POST metadelete with id=<recipe_metaId>
|
||||||
|
|
||||||
|
Newline normalisation (bug fix v0.6.1):
|
||||||
|
LLM tool calls may pass literal backslash-n ('\\n', two chars) instead of a
|
||||||
|
real newline character. _normalize_newlines() converts them before sending
|
||||||
|
to the API so the server splits ingredients/instructions correctly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -11,6 +17,23 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_newlines(text: str) -> str:
|
||||||
|
"""Replace literal backslash-n sequences with real newline characters.
|
||||||
|
|
||||||
|
When an LLM generates a tool call it may produce ``"line1\\nline2"``
|
||||||
|
(two chars: backslash + n) instead of ``"line1\\nline2"`` (real newline).
|
||||||
|
The Family Wall API splits ingredients and instructions on real newlines
|
||||||
|
only, so we normalise before sending.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Free-text string that may contain literal ``\\n`` sequences.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The same string with every ``\\n`` replaced by a real ``\\n``.
|
||||||
|
"""
|
||||||
|
return text.replace("\\n", "\n")
|
||||||
|
|
||||||
|
|
||||||
def parse_recipe_summary(raw: dict[str, Any]) -> dict[str, Any]:
|
def parse_recipe_summary(raw: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Extract a compact recipe summary from a raw API recipe object.
|
"""Extract a compact recipe summary from a raw API recipe object.
|
||||||
|
|
||||||
@@ -86,11 +109,17 @@ def build_create_params(
|
|||||||
The Family Wall API requires the 'recipe.' prefix for all recipe fields.
|
The Family Wall API requires the 'recipe.' prefix for all recipe fields.
|
||||||
isRecipe='true' is always required.
|
isRecipe='true' is always required.
|
||||||
|
|
||||||
|
Newline normalisation is applied to ingredients and instructions so that
|
||||||
|
literal ``\\n`` sequences sent by an LLM are converted to real newlines,
|
||||||
|
which the server uses to split the ingredient list.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: Recipe title (required).
|
name: Recipe title (required).
|
||||||
description: Optional description.
|
description: Optional description.
|
||||||
ingredients: Optional free-text ingredients, lines separated by \\n.
|
ingredients: Optional free-text ingredients, lines separated by ``\\n``.
|
||||||
instructions: Optional free-text cooking instructions, lines separated by \\n.
|
Literal backslash-n sequences are normalised to real newlines.
|
||||||
|
instructions: Optional free-text cooking instructions, lines separated
|
||||||
|
by ``\\n``. Literal backslash-n sequences are normalised.
|
||||||
prep_time_minutes: Optional preparation time in minutes.
|
prep_time_minutes: Optional preparation time in minutes.
|
||||||
cook_time_minutes: Optional cooking time in minutes.
|
cook_time_minutes: Optional cooking time in minutes.
|
||||||
serves: Optional number of servings.
|
serves: Optional number of servings.
|
||||||
@@ -106,9 +135,69 @@ def build_create_params(
|
|||||||
if description is not None:
|
if description is not None:
|
||||||
params["recipe.description"] = description
|
params["recipe.description"] = description
|
||||||
if ingredients is not None:
|
if ingredients is not None:
|
||||||
params["recipe.ingredients"] = ingredients
|
params["recipe.ingredients"] = _normalize_newlines(ingredients)
|
||||||
if instructions is not None:
|
if instructions is not None:
|
||||||
params["recipe.instructions"] = instructions
|
params["recipe.instructions"] = _normalize_newlines(instructions)
|
||||||
|
if prep_time_minutes is not None:
|
||||||
|
params["recipe.prepTime"] = str(prep_time_minutes)
|
||||||
|
if cook_time_minutes is not None:
|
||||||
|
params["recipe.cookTime"] = str(cook_time_minutes)
|
||||||
|
if serves is not None:
|
||||||
|
params["recipe.serves"] = str(serves)
|
||||||
|
if url is not None:
|
||||||
|
params["recipe.url"] = url
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def build_update_params(
|
||||||
|
recipe_id: str,
|
||||||
|
current_name: str,
|
||||||
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
ingredients: str | None = None,
|
||||||
|
instructions: str | None = None,
|
||||||
|
prep_time_minutes: int | None = None,
|
||||||
|
cook_time_minutes: int | None = None,
|
||||||
|
serves: int | None = None,
|
||||||
|
url: str | None = None,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Build the form parameters for a mprecipeput update call.
|
||||||
|
|
||||||
|
Identical to :func:`build_create_params` except that ``recipe.metaId`` is
|
||||||
|
included so the API updates the existing recipe instead of creating a new one.
|
||||||
|
``recipe.name`` and ``recipe.isRecipe`` are always present — the name falls
|
||||||
|
back to *current_name* when the caller does not provide a new one.
|
||||||
|
|
||||||
|
Newline normalisation is applied to ingredients and instructions (see
|
||||||
|
:func:`_normalize_newlines`).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recipe_id: metaId of the recipe to update (e.g. ``"recipe/123_456"``).
|
||||||
|
current_name: The recipe's existing name, used as fallback when *name*
|
||||||
|
is not provided. Fetched from the API before calling this function.
|
||||||
|
name: New recipe title (omit to keep existing).
|
||||||
|
description: New description (omit to keep existing).
|
||||||
|
ingredients: New ingredients text (omit to keep existing).
|
||||||
|
instructions: New instructions text (omit to keep existing).
|
||||||
|
prep_time_minutes: New preparation time in minutes (omit to keep existing).
|
||||||
|
cook_time_minutes: New cooking time in minutes (omit to keep existing).
|
||||||
|
serves: New number of servings (omit to keep existing).
|
||||||
|
url: New external URL (omit to keep existing).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict of form parameters ready to send to mprecipeput.
|
||||||
|
"""
|
||||||
|
params: dict[str, str] = {
|
||||||
|
"recipe.metaId": recipe_id,
|
||||||
|
"recipe.name": name if name is not None else current_name,
|
||||||
|
"recipe.isRecipe": "true",
|
||||||
|
}
|
||||||
|
if description is not None:
|
||||||
|
params["recipe.description"] = description
|
||||||
|
if ingredients is not None:
|
||||||
|
params["recipe.ingredients"] = _normalize_newlines(ingredients)
|
||||||
|
if instructions is not None:
|
||||||
|
params["recipe.instructions"] = _normalize_newlines(instructions)
|
||||||
if prep_time_minutes is not None:
|
if prep_time_minutes is not None:
|
||||||
params["recipe.prepTime"] = str(prep_time_minutes)
|
params["recipe.prepTime"] = str(prep_time_minutes)
|
||||||
if cook_time_minutes is not None:
|
if cook_time_minutes is not None:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from mcp_familywall.fw_client import FamilyWallClient, FamilyWallError
|
|||||||
from mcp_familywall.modules.lists import translate_name
|
from mcp_familywall.modules.lists import translate_name
|
||||||
from mcp_familywall.modules.recipes import (
|
from mcp_familywall.modules.recipes import (
|
||||||
build_create_params,
|
build_create_params,
|
||||||
|
build_update_params,
|
||||||
parse_recipe_full,
|
parse_recipe_full,
|
||||||
parse_recipe_summary,
|
parse_recipe_summary,
|
||||||
)
|
)
|
||||||
@@ -1337,6 +1338,142 @@ def delete_recipe(recipe_id: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool: update_recipe
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def update_recipe(
|
||||||
|
recipe_id: str,
|
||||||
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
ingredients: str | None = None,
|
||||||
|
instructions: str | None = None,
|
||||||
|
prep_time_minutes: int | None = None,
|
||||||
|
cook_time_minutes: int | None = None,
|
||||||
|
serves: int | None = None,
|
||||||
|
url: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Update an existing recipe in the Family Wall recipe box.
|
||||||
|
|
||||||
|
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||||
|
|
||||||
|
At least one field besides *recipe_id* must be provided. Fields that are
|
||||||
|
omitted are left unchanged on the server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recipe_id: Recipe metaId from get_recipes
|
||||||
|
(e.g. ``"recipe/23431854_10968866"``).
|
||||||
|
name: New recipe title (omit to keep existing).
|
||||||
|
description: New description (omit to keep existing).
|
||||||
|
ingredients: New ingredient list as free text (omit to keep existing).
|
||||||
|
Use newlines (``\\n``) to separate items.
|
||||||
|
Example: ``"200g Mehl\\n3 Eier\\n100ml Milch"``
|
||||||
|
instructions: New cooking instructions as free text (omit to keep
|
||||||
|
existing). Use newlines (``\\n``) to separate steps.
|
||||||
|
prep_time_minutes: New preparation time in minutes (omit to keep
|
||||||
|
existing).
|
||||||
|
cook_time_minutes: New cooking/baking time in minutes (omit to keep
|
||||||
|
existing).
|
||||||
|
serves: New number of servings (omit to keep existing).
|
||||||
|
url: New external URL (omit to keep existing).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON with the updated recipe's full fields on success, or an error
|
||||||
|
message.
|
||||||
|
"""
|
||||||
|
if all(
|
||||||
|
v is None
|
||||||
|
for v in (
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
ingredients,
|
||||||
|
instructions,
|
||||||
|
prep_time_minutes,
|
||||||
|
cook_time_minutes,
|
||||||
|
serves,
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
"Error: At least one of 'name', 'description', 'ingredients', 'instructions',"
|
||||||
|
" 'prep_time_minutes', 'cook_time_minutes', 'serves', or 'url' must be provided."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Single session: fetch current recipe (verify + get name fallback) → update.
|
||||||
|
try:
|
||||||
|
email, password = get_credentials()
|
||||||
|
except RuntimeError as exc:
|
||||||
|
return f"Error: {exc}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with FamilyWallClient() as client:
|
||||||
|
client.login(email, password)
|
||||||
|
|
||||||
|
# Fetch all recipes to verify the target exists and is updatable.
|
||||||
|
raw_data = client.call("metasync", {"id": "recipe"})
|
||||||
|
try:
|
||||||
|
items: list[dict[str, Any]] = raw_data["a00"]["r"]["r"]["updatedCreated"]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
items = []
|
||||||
|
|
||||||
|
current = next((r for r in items if r.get("metaId") == recipe_id), None)
|
||||||
|
if current is None:
|
||||||
|
client.logout()
|
||||||
|
return f"Error: Recipe '{recipe_id}' not found."
|
||||||
|
|
||||||
|
can_update: str | None = (current.get("rights") or {}).get("canUpdate")
|
||||||
|
if can_update != "true":
|
||||||
|
client.logout()
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"error": "Recipe cannot be updated.",
|
||||||
|
"id": recipe_id,
|
||||||
|
"name": current.get("name"),
|
||||||
|
"hint": "Only recipes you created (can_update=true) can be updated.",
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build params — current name is used as fallback when caller omits name.
|
||||||
|
params = build_update_params(
|
||||||
|
recipe_id=recipe_id,
|
||||||
|
current_name=current.get("name", ""),
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
ingredients=ingredients,
|
||||||
|
instructions=instructions,
|
||||||
|
prep_time_minutes=prep_time_minutes,
|
||||||
|
cook_time_minutes=cook_time_minutes,
|
||||||
|
serves=serves,
|
||||||
|
url=url,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.call("mprecipeput", params)
|
||||||
|
client.logout()
|
||||||
|
except FamilyWallError as exc:
|
||||||
|
return f"Error: Family Wall API error: {exc}"
|
||||||
|
except Exception as exc:
|
||||||
|
return f"Error: Connection error: {exc}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
recipe_obj = resp["a00"]["r"]["r"]
|
||||||
|
if not isinstance(recipe_obj, dict) or "metaId" not in recipe_obj:
|
||||||
|
raise TypeError("unexpected shape")
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
return json.dumps(
|
||||||
|
{"warning": "Unexpected mprecipeput response structure", "raw": resp},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = parse_recipe_full(recipe_obj)
|
||||||
|
result["updated"] = True
|
||||||
|
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Factory
|
# Factory
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user