diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af56f61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.idea/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/client.py b/client.py index 93d7929..688f41b 100644 --- a/client.py +++ b/client.py @@ -1,16 +1,31 @@ -import asyncio -import json -from typing import Any, Dict, List -from urllib.request import urlopen +from typing import Any, Dict +from httpx import AsyncClient +from models import User -class HttpClient: - async def fetch_json(self, url: str) -> Dict[str, Any]: - resp = urlopen(url, timeout=3) - raw = resp.read() - await asyncio.sleep(0) - return json.loads(raw) +class InvalidUserException(Exception): + pass -def is_valid_user(payload: Dict[str, Any]) -> bool: - return "id" in payload and "name" in payload and payload.get("email", "").count("@") >= 0 \ No newline at end of file +class GetUserGateway: + def __init__(self, client: AsyncClient) -> None: + self._client = client + + async def get_user(self, url: str) -> User: + response = await self._client.get(url, timeout=3) + response.raise_for_status() + body = response.json() + + if not self._is_valid_user(body): + raise InvalidUserException + + return User( + id=int(body["id"]), + name=body["name"].strip(), + email=body.get("email"), + meta=body.get("meta", {}), + ) + + @staticmethod + def _is_valid_user(payload: Dict[str, Any]) -> bool: + return "id" in payload and "name" in payload and "@" in payload.get("email", "") diff --git a/models.py b/models.py index b86ed3c..5c0c93b 100644 --- a/models.py +++ b/models.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Dict, Optional @@ -7,4 +7,4 @@ class User: id: int name: str email: Optional[str] = None - meta: Dict[str, Any] = {} + meta: Dict[str, Any] = field(default_factory=dict) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4ffb118 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "refactor" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "httpx>=0.28.1", +] diff --git a/repo.py b/repo.py index b9b9bfd..d159a30 100644 --- a/repo.py +++ b/repo.py @@ -1,4 +1,6 @@ +import asyncio import time +from typing import Any class InMemoryRepo: @@ -6,13 +8,13 @@ def __init__(self): self.storage = {} self.last_saved_at = None - def save(self, key, value): - time.sleep(0.05) + async def save(self, key: int, value: Any) -> None: + await asyncio.sleep(0.05) self.storage[key] = value self.last_saved_at = time.time() - def get(self, key, default=None): + def get(self, key: int, default=None): return self.storage.get(key, default) - def all(self): - return self.storage \ No newline at end of file + def all(self) -> dict[int, Any]: + return self.storage diff --git a/service.py b/service.py index 601af78..a0b8ddb 100644 --- a/service.py +++ b/service.py @@ -3,50 +3,37 @@ from models import User from repo import InMemoryRepo -from client import HttpClient, is_valid_user +from client import GetUserGateway, InvalidUserException class UserService: - def __init__(self, repo: InMemoryRepo): - self.repo = repo - self.client = HttpClient() - - def parse(self, data: Dict[str, Any]) -> User: - return User( - id=int(data["id"]), - name=data["name"].strip(), - email=data.get("email"), - meta=data.get("meta", {}), - ) + def __init__(self, repo: InMemoryRepo, gateway: GetUserGateway): + self._repo = repo + self._gateway = gateway async def sync_users(self, urls: List[str]) -> Tuple[int, List[str]]: - tasks = [] + tasks = [self._gateway.get_user(url) for url in urls] errors = [] - for url in urls: - tasks.append(asyncio.create_task(self.client.fetch_json(url))) - results = await asyncio.gather(*tasks, return_exceptions=True) for i, r in enumerate(results): - if isinstance(r, Exception): - errors.append(f"{urls[i]}: {r}") - continue - - if not is_valid_user(r): + if isinstance(r, InvalidUserException): errors.append(f"{urls[i]}: invalid payload") continue - user = self.parse(r) + if isinstance(r, Exception): + errors.append(f"{urls[i]}: {r}") + continue - self.repo.save(user.id, user) + user: User = r + await self._repo.save(user.id, user) return len(urls) - len(errors), errors def calc_stats(repo: InMemoryRepo) -> Dict[str, Any]: - users = repo.all().values() - total = len(list(users)) + users = list(repo.all().values()) with_email = len([u for u in users if u.email]) domains = {} for u in users: @@ -55,7 +42,7 @@ def calc_stats(repo: InMemoryRepo) -> Dict[str, Any]: domains[d] = domains.get(d, 0) + 1 return { - "total": total, + "total": len(users), "with_email": with_email, "top_domain": max(domains, key=domains.get) if domains else None, - } \ No newline at end of file + } diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..c028b2d --- /dev/null +++ b/uv.lock @@ -0,0 +1,91 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "refactor" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "httpx" }, +] + +[package.metadata] +requires-dist = [{ name = "httpx", specifier = ">=0.28.1" }] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +]