diff --git a/README.md b/README.md index 9451697..643e805 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ Built with **FastAPI & Pydantic v2** and fewer than 100 SLOC of business code. ### Quick start ```bash -git clone https://github.com/your‑org/phonebook‑rpc.git -cd phonebook‑rpc python -m venv .venv && source .venv/bin/activate # Windows: .venv\\Scripts\\activate pip install -r requirements.txt uvicorn app.main:app --reload # http://127.0.0.1:5000 @@ -35,12 +33,14 @@ tests/ ### JSON‑RPC API -| Method | Params | Result / Error | -|-------------------|-----------------------------------------------------|----------------| -| **AddContact** | `name:str`, `phone:str`, `email:str\|null` | `Contact` | -| **GetByName** | `name:str` | `Contact[]` | -| **UpdateContact** | `id:str`, `name:str`, `phone:str`, `email:str|null` | `Contact` | -| **DeleteContact** | `id:str` | `null` | + +| Method | Params | Result / Error | +|--------------------|-----------------------------------------------------|---------------------------------| +| **AddContact** | `name:str`, `phone:str`, `email:str\|null` | `Contact` | +| **GetByName** | `name:str` | `Contact[]` | +| **GetAllContacts** | _none_ | `Dict[str, List[str]]` | +| **UpdateContact** | `id:str`, `name:str`, `phone:str`, `email:str\|null` | `Contact` | +| **DeleteContact** | `id:str` | `null` | > *All phone numbers must match E.164: `+` and 7–15 digits.* > Duplicate (`name + phone`) raises `"code":‑32602`. @@ -100,6 +100,16 @@ tests/ } ``` +**5. Get all contacts:** +```json +{ + "jsonrpc":"2.0", + "method":"GetAllContacts", + "params":{}, + "id":5 +} +``` + --- ### Running the tests @@ -125,14 +135,6 @@ TOTAL 125 0 100% --- -### Why JSON‑RPC instead of REST? - -* single `/rpc` endpoint; method name lives in the payload -* symmetrical request / response with formal spec (OpenRPC‑ready) -* thin ~20‑line dispatcher, **no** external dependency (`fastapi-jsonrpc` not required) - ---- - ### Requirements * Python 3.10+ diff --git a/app/PhoneBook.py b/app/PhoneBook.py index 7fd7b83..2cf9615 100644 --- a/app/PhoneBook.py +++ b/app/PhoneBook.py @@ -46,6 +46,13 @@ async def get_by_name(self, name: str) -> List[Contact]: logger.debug("GetByName '%s' -> %d records", name, len(contacts)) return contacts + async def get_all(self) -> Dict[str, List[str]]: + async with self._lock: + result: Dict[str, List[str]] = {} + for name, ids in self._by_name.items(): + result[name] = [self._by_id[i] for i in ids] + return result + async def update(self, id: str, name: str, phone: str, email: Optional[str] = None) -> Contact: async with self._lock: if id not in self._by_id: diff --git a/app/router.py b/app/router.py index 2a75751..61abc70 100644 --- a/app/router.py +++ b/app/router.py @@ -17,8 +17,8 @@ async def _json_success(result: Any, id_: Optional[str]) -> Dict[str, Any]: AUTHORISATION_ERROR_CODES = { - KeyError: (-32602, "Not found"), - ValueError: (-32602, "Invalid params"), + KeyError: (-32602, "Not found"), + ValueError: (-32602, None), } @@ -58,6 +58,10 @@ async def rpc_entrypoint(request: Request) -> Dict[str, Any]: await phonebook.delete(params["id"]) resp = await _json_success(None, id_) + elif method == "GetAllContacts": + all_contacts = await phonebook.get_all() + resp = await _json_success(all_contacts, id_) + else: resp = {"jsonrpc": "2.0", "error": {"code": -32601, "message": f"Unknown method {method!r}"}, "id": id_} @@ -65,7 +69,12 @@ async def rpc_entrypoint(request: Request) -> Dict[str, Any]: return resp except Exception as exc: - code, message = AUTHORISATION_ERROR_CODES.get(type(exc), (-32000, str(exc))) - resp = {"jsonrpc": "2.0", "error": {"code": code, "message": message}, "id": id_} + code, default_msg = AUTHORISATION_ERROR_CODES.get(type(exc), (-32000, None)) + message = default_msg if default_msg is not None else str(exc) + resp = { + "jsonrpc": "2.0", + "error": {"code": code, "message": message}, + "id": id_, + } log.warning("! %s -> %s", method, message) return resp \ No newline at end of file diff --git a/devops/Dockerfile b/devops/Dockerfile index c4070ed..3e6c052 100644 --- a/devops/Dockerfile +++ b/devops/Dockerfile @@ -22,5 +22,4 @@ ENV PYTHONUNBUFFERED=1 \ EXPOSE 8000 -CMD ["uvicorn", "--factory", "--workers", "4", \ - "--host", "0.0.0.0", "--port", "8000", "app.main:app"] +CMD ["uvicorn", "--workers", "4", "--host", "0.0.0.0", "--port", "8000", "app.main:app"] diff --git a/tests/test_rpc.py b/tests/test_rpc.py index c56c1eb..8a31b99 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -88,3 +88,41 @@ async def test_invalid_jsonrpc_version(client): assert data["error"]["code"] == -32600 assert data["id"] is None + +@pytest.mark.asyncio +async def test_get_all_contacts(client): + # Добавим двух пользователей + await client.post("/rpc", json={ + "jsonrpc": "2.0", + "method": "AddContact", + "params": {"name": "Bob", "phone": "+79990000001"}, + "id": 1 + }) + await client.post("/rpc", json={ + "jsonrpc": "2.0", + "method": "AddContact", + "params": {"name": "Charlie", "phone": "+79990000002"}, + "id": 2 + }) + + # Получим всех + res = await client.post("/rpc", json={ + "jsonrpc": "2.0", + "method": "GetAllContacts", + "params": {}, + "id": 3 + }) + data = res.json() + + assert "result" in data + result = data["result"] + assert isinstance(result, dict) + assert "bob" in result or "charlie" in result # имя преобразуется к lowercase + for name, contacts in result.items(): + assert isinstance(name, str) + assert isinstance(contacts, list) + for contact in contacts: + assert "id" in contact + assert "name" in contact + assert "phone" in contact +