From 70afd51bbb31c1f281ed79978d99b6a7f006df9b Mon Sep 17 00:00:00 2001 From: r3build Date: Tue, 22 Apr 2025 19:44:17 +0300 Subject: [PATCH 1/6] Update Dockerfile --- devops/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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"] From 3952c640a9ad0c39370a115a33f7812d8282767c Mon Sep 17 00:00:00 2001 From: EmilGoryachih Date: Mon, 28 Apr 2025 12:22:33 +0300 Subject: [PATCH 2/6] Added method getAll --- README.md | 24 ++++++++++++++++++------ app/PhoneBook.py | 7 +++++++ app/router.py | 17 +++++++++++++---- tests/test_rpc.py | 23 +++++++++++++++++++++++ 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9451697..a468d68 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,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 +102,16 @@ tests/ } ``` +**5. Get all contacts:** +```json +{ + "jsonrpc":"2.0", + "method":"GetAllContacts", + "params":{}, + "id":5 +} +``` + --- ### Running the tests diff --git a/app/PhoneBook.py b/app/PhoneBook.py index 7fd7b83..ccb93cd 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].phone 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/tests/test_rpc.py b/tests/test_rpc.py index c56c1eb..3a589d3 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -88,3 +88,26 @@ 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":"X","phone":"+70000001"},"id":100 + }) + await client.post("/rpc", json={ + "jsonrpc":"2.0","method":"AddContact", + "params":{"name":"Y","phone":"+70000002"},"id":101 + }) + + r = await client.post("/rpc", json={ + "jsonrpc":"2.0","method":"GetAllContacts", + "params":{}, "id":102 + }) + data = r.json()["result"] + # data должно быть dict: {"x":[...], "y":[...]} + assert "+70000001" in data["x"] + assert "+70000002" in data["y"] + + From 0125f854b15197d71add5ab0fb3254b719e6fbc7 Mon Sep 17 00:00:00 2001 From: EmilGoryachih Date: Tue, 29 Apr 2025 12:57:51 +0300 Subject: [PATCH 3/6] Changed getAll --- app/PhoneBook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/PhoneBook.py b/app/PhoneBook.py index ccb93cd..2cf9615 100644 --- a/app/PhoneBook.py +++ b/app/PhoneBook.py @@ -50,7 +50,7 @@ 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].phone for i in ids] + 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: From 92c13090faab06b3a14aacd5ae87cf024a1a9bb1 Mon Sep 17 00:00:00 2001 From: EmilGoryachih Date: Tue, 29 Apr 2025 12:59:27 +0300 Subject: [PATCH 4/6] Changed test --- tests/test_rpc.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 3a589d3..bfb5658 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -89,25 +89,3 @@ async def test_invalid_jsonrpc_version(client): 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":"X","phone":"+70000001"},"id":100 - }) - await client.post("/rpc", json={ - "jsonrpc":"2.0","method":"AddContact", - "params":{"name":"Y","phone":"+70000002"},"id":101 - }) - - r = await client.post("/rpc", json={ - "jsonrpc":"2.0","method":"GetAllContacts", - "params":{}, "id":102 - }) - data = r.json()["result"] - # data должно быть dict: {"x":[...], "y":[...]} - assert "+70000001" in data["x"] - assert "+70000002" in data["y"] - - From 3d6fadf9a302a5af5af07f5f548ee437032dc317 Mon Sep 17 00:00:00 2001 From: EmilGoryachih Date: Tue, 29 Apr 2025 21:39:38 +0300 Subject: [PATCH 5/6] Added test --- tests/test_rpc.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index bfb5658..8a31b99 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -89,3 +89,40 @@ async def test_invalid_jsonrpc_version(client): 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 + From 64c938898e61471b60b791118f816c6978997001 Mon Sep 17 00:00:00 2001 From: EmilGoryachih Date: Tue, 29 Apr 2025 23:04:47 +0300 Subject: [PATCH 6/6] Changed README --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index a468d68..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 @@ -137,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+