xn-auth 0.2.66__tar.gz → 0.2.68.dev1__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.
Files changed (25) hide show
  1. xn_auth-0.2.68.dev1/.env.dist +3 -0
  2. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/PKG-INFO +4 -1
  3. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/pyproject.toml +9 -1
  4. xn_auth-0.2.68.dev1/tests/test_proxy.py +86 -0
  5. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/x_auth/models.py +15 -30
  6. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/x_auth/types.py +3 -5
  7. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/xn_auth.egg-info/PKG-INFO +4 -1
  8. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/xn_auth.egg-info/SOURCES.txt +1 -0
  9. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/xn_auth.egg-info/requires.txt +3 -0
  10. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/xn_auth.egg-info/scm_file_list.json +9 -8
  11. xn_auth-0.2.68.dev1/xn_auth.egg-info/scm_version.json +8 -0
  12. xn_auth-0.2.66/.env.dist +0 -1
  13. xn_auth-0.2.66/xn_auth.egg-info/scm_version.json +0 -8
  14. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/.gitignore +0 -0
  15. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/.pre-commit-config.yaml +0 -0
  16. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/CLAUDE.md +0 -0
  17. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/README.md +0 -0
  18. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/makefile +0 -0
  19. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/setup.cfg +0 -0
  20. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/x_auth/controller.py +0 -0
  21. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/x_auth/enums.py +0 -0
  22. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/x_auth/exceptions.py +0 -0
  23. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/x_auth/middleware.py +0 -0
  24. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/xn_auth.egg-info/dependency_links.txt +0 -0
  25. {xn_auth-0.2.66 → xn_auth-0.2.68.dev1}/xn_auth.egg-info/top_level.txt +0 -0
@@ -0,0 +1,3 @@
1
+ VENV=.venv
2
+ WS_TOKEN=
3
+ PG_DSN=
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xn-auth
3
- Version: 0.2.66
3
+ Version: 0.2.68.dev1
4
4
  Summary: Auth adapter for XN-Api framework
5
5
  Author-email: Artemiev <mixartemev@gmail.com>
6
6
  License: MIT
@@ -17,7 +17,10 @@ Requires-Dist: pyjwt
17
17
  Requires-Dist: xn-model
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: build; extra == "dev"
20
+ Requires-Dist: pre-commit; extra == "dev"
20
21
  Requires-Dist: python-dotenv; extra == "dev"
22
+ Requires-Dist: pytest; extra == "dev"
23
+ Requires-Dist: pytest-asyncio; extra == "dev"
21
24
  Requires-Dist: twine; extra == "dev"
22
25
 
23
26
  # X-Auth
@@ -22,7 +22,10 @@ dependencies = [
22
22
  [project.optional-dependencies]
23
23
  dev = [
24
24
  "build",
25
+ "pre-commit",
25
26
  "python-dotenv",
27
+ "pytest",
28
+ "pytest-asyncio",
26
29
  "twine"
27
30
  ]
28
31
 
@@ -42,4 +45,9 @@ version_scheme = "python-simplified-semver" # if "feature" in `branch_name` SEMV
42
45
  local_scheme = "no-local-version"
43
46
 
44
47
  [tool.ruff]
45
- line-length = 120
48
+ line-length = 120
49
+
50
+ [tool.pytest.ini_options]
51
+ asyncio_mode = "strict"
52
+ markers = ["live: реально ходит в webshare, нужен WST"]
53
+ addopts = "-m 'not live'"
@@ -0,0 +1,86 @@
1
+ """Реальные вызовы Proxy: живые запросы в webshare, load_list ещё и пишет в базу."""
2
+
3
+ from datetime import datetime
4
+ from os import getenv
5
+
6
+ import pytest
7
+ import pytest_asyncio
8
+ from dotenv import load_dotenv
9
+ from tortoise import Tortoise
10
+
11
+ from x_auth.models import Proxy
12
+ from x_auth.types import Replacement
13
+
14
+ load_dotenv()
15
+
16
+ WST = getenv("WS_TOKEN")
17
+
18
+ pytestmark = pytest.mark.asyncio
19
+
20
+
21
+ @pytest_asyncio.fixture
22
+ async def db():
23
+ await Tortoise.init(
24
+ {
25
+ "connections": {"default": getenv("PG_DSN")},
26
+ "apps": {"models": {"models": ["x_auth.models"]}},
27
+ "use_tz": True,
28
+ "timezone": "UTC",
29
+ }
30
+ )
31
+ yield
32
+ await Tortoise.close_connections()
33
+
34
+
35
+ async def test_load_list(db):
36
+ prxs = await Proxy.get_list(WST)
37
+ assert prxs, "webshare вернул пустой список"
38
+ was = await Proxy.all().count()
39
+
40
+ await Proxy.load_list(WST)
41
+
42
+ print(f"\nиз webshare {len(prxs)} шт., в базе было {was}, стало {await Proxy.all().count()}")
43
+ for r in prxs:
44
+ prx = await Proxy.get_or_none(id=r.id)
45
+ assert prx, f"{r.host}:{r.port} (id={r.id}) не доехал до базы"
46
+ assert (prx.host, prx.port) == (r.host, r.port)
47
+ assert (prx.username, prx.password) == (r.username, r.password)
48
+ assert prx.valid is r.valid
49
+ # last_verification из webshare до базы не доезжает: TsTrait.updated_at это auto_now
50
+ assert prx.updated_at != r.updated_at
51
+ country = await prx.country
52
+ assert country.short == r.country_code
53
+ print(f" {prx.id} {country.short} {prx.str()} valid={prx.valid} upd={prx.updated_at:%F %T}")
54
+
55
+
56
+ async def test_load_list_idempotent(db):
57
+ """Повторный прогон не должен плодить дубли: апсерт идёт по pk и по (host, port)."""
58
+ await Proxy.load_list(WST)
59
+ was = await Proxy.all().count()
60
+
61
+ await Proxy.load_list(WST)
62
+
63
+ assert await Proxy.all().count() == was
64
+
65
+
66
+ async def test_get_replaced():
67
+ """В базу не ходит, поэтому без фикстуры db."""
68
+ reps = await Proxy.get_replaced(WST)
69
+
70
+ assert isinstance(reps, list)
71
+ if not reps:
72
+ pytest.skip("у аккаунта нет замен (count=0) - разбор ответа проверить не на чем")
73
+
74
+ print(f"\nзамен: {len(reps)}")
75
+ for r in reps:
76
+ print(f" {r.created_at:%F %T} {r.reason}: {r.proxy}:{r.proxy_port} ({r.proxy_country_code})")
77
+ print(f" -> {r.replaced_with}:{r.replaced_with_port} ({r.replaced_with_country_code})")
78
+
79
+ for r in reps:
80
+ assert isinstance(r, Replacement)
81
+ # reason в структуре Literal - на неизвестном значении convert упал бы ещё в get_replaced
82
+ assert r.reason in ("auto_invalidated", "auto_out_of_rotation")
83
+ assert r.proxy and r.proxy_port
84
+ assert r.replaced_with and r.replaced_with_port
85
+ assert len(r.proxy_country_code) == len(r.replaced_with_country_code) == 2
86
+ assert isinstance(r.created_at, datetime)
@@ -4,6 +4,7 @@ from aiogram.utils.web_app import WebAppUser
4
4
  from aiohttp import ClientSession
5
5
  from litestar.exceptions import NotFoundException
6
6
  from msgspec import convert
7
+ from msgspec.structs import asdict
7
8
  from pyrogram.enums.client_platform import ClientPlatform
8
9
  from pyrogram.types import User as PyroUser
9
10
  from aiogram.types import User as AioUser
@@ -117,21 +118,6 @@ class Proxy(Model, TsTrait):
117
118
  class Meta:
118
119
  unique_together = (("host", "port"),)
119
120
 
120
- @staticmethod
121
- async def load(wst: str):
122
- async with ClientSession("https://proxy.webshare.io/api/v2/") as s:
123
- resp = await s.post("download_token/proxy_list/", headers={"Authorization": f"Token {wst}"})
124
- dtok = (await resp.json())["key"]
125
- resp = await s.get(f"proxy/list/download/{dtok}/-/any/username/direct/-/")
126
- res = (await resp.text()).split("\r\n")
127
- proxies = [
128
- Proxy(host=p[0], port=p[1], username=p[2], password=p[3], success=True)
129
- for r in res
130
- if r and (p := r.split(":"))
131
- ]
132
- await Proxy.bulk_create(proxies, 10, False, ["username", "password", "success"], ["host", "port"])
133
- return proxies
134
-
135
121
  @staticmethod
136
122
  async def get_list(wst: str) -> list[types.Proxy]:
137
123
  async with ClientSession("https://proxy.webshare.io/api/v2/") as s:
@@ -146,19 +132,16 @@ class Proxy(Model, TsTrait):
146
132
  ccl = cls._meta.fields_map["country"].related_model
147
133
  cmap = dict(await ccl.filter(short__in=[p.country_code for p in prxs]).values_list("short", "id"))
148
134
  for r in prxs:
149
- df = dict(
150
- username=r.username,
151
- password=r.password,
152
- valid=r.valid,
153
- country_id=cmap[r.country_code],
154
- updated_at=r.last_verification,
155
- )
156
- if prx := await cls.get_or_none(host=r.proxy_address, port=r.port):
157
- await prx.update_from_dict({**df, "id": r.id}).save()
158
- elif prx := await cls.get_or_none(id=r.id):
159
- await prx.update_from_dict({**df, "host": r.proxy_address, "port": r.port}).save()
135
+ df = asdict(r) # имена полей структуры уже совпадают с колонками модели
136
+ pk, host, port = df.pop("id"), df.pop("host"), df.pop("port")
137
+ df["country_id"] = cmap[df.pop("country_code")]
138
+ # у webshare независимо меняются и id, и пара (host, port) - ищем по обоим уникальным ключам
139
+ if prx := await cls.get_or_none(host=host, port=port):
140
+ await prx.update_from_dict({**df, "id": pk}).save()
141
+ elif prx := await cls.get_or_none(id=pk):
142
+ await prx.update_from_dict({**df, "host": host, "port": port}).save()
160
143
  else:
161
- await cls.create(**df, host=r.proxy_address, port=r.port, id=r.id)
144
+ await cls.create(**df, id=pk, host=host, port=port)
162
145
 
163
146
  @staticmethod
164
147
  async def get_replaced(wst: str):
@@ -214,13 +197,15 @@ class Session(TortModel):
214
197
  api_id: int
215
198
  dc: ForeignKeyRelation[Dc] = ForeignKeyField("models.Dc", "sessions", on_update=CASCADE, db_default=2)
216
199
  dc_id: int
217
- test_mode = BooleanField(db_default=False)
200
+ # default дублирует db_default не зря: при явном pk tortoise вставляет строку целиком и шлёт по
201
+ # таким колонкам NULL вместо того, что б их опустить и дать сработать дефолту базы
202
+ test_mode = BooleanField(db_default=False, default=False)
218
203
  auth_key = BinaryField(null=True)
219
- date = IntField(db_default=0) # todo: refact to datetime?
204
+ date = IntField(db_default=0, default=0) # todo: refact to datetime?
220
205
  user: OneToOneNullableRelation[Username] = OneToOneField("models.Username", "session", on_update=CASCADE, null=True)
221
206
  user_id: int
222
207
  is_bot = CharField(42, null=True)
223
- state: dict = JSONField(db_default={})
208
+ state: dict = JSONField(db_default={}, default=dict)
224
209
  proxy: ForeignKeyNullableRelation[Proxy] = ForeignKeyField("models.Proxy", "sessions", on_update=CASCADE, null=True)
225
210
 
226
211
  peers: BackwardFKRelation["Peer"]
@@ -14,17 +14,15 @@ class AuthUser(Struct):
14
14
  role: Role
15
15
 
16
16
 
17
- class Proxy(Struct):
17
+ class Proxy(Struct, rename={"host": "proxy_address", "updated_at": "last_verification"}):
18
18
  id: str
19
19
  username: str
20
20
  password: str
21
- proxy_address: str
21
+ host: str
22
22
  port: int
23
23
  valid: bool
24
- last_verification: datetime
24
+ updated_at: datetime
25
25
  country_code: str
26
- city_name: str
27
- created_at: datetime
28
26
 
29
27
 
30
28
  class Replacement(Struct):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xn-auth
3
- Version: 0.2.66
3
+ Version: 0.2.68.dev1
4
4
  Summary: Auth adapter for XN-Api framework
5
5
  Author-email: Artemiev <mixartemev@gmail.com>
6
6
  License: MIT
@@ -17,7 +17,10 @@ Requires-Dist: pyjwt
17
17
  Requires-Dist: xn-model
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: build; extra == "dev"
20
+ Requires-Dist: pre-commit; extra == "dev"
20
21
  Requires-Dist: python-dotenv; extra == "dev"
22
+ Requires-Dist: pytest; extra == "dev"
23
+ Requires-Dist: pytest-asyncio; extra == "dev"
21
24
  Requires-Dist: twine; extra == "dev"
22
25
 
23
26
  # X-Auth
@@ -5,6 +5,7 @@ CLAUDE.md
5
5
  README.md
6
6
  makefile
7
7
  pyproject.toml
8
+ tests/test_proxy.py
8
9
  x_auth/controller.py
9
10
  x_auth/enums.py
10
11
  x_auth/exceptions.py
@@ -7,5 +7,8 @@ xn-model
7
7
 
8
8
  [dev]
9
9
  build
10
+ pre-commit
10
11
  python-dotenv
12
+ pytest
13
+ pytest-asyncio
11
14
  twine
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "files": [
3
- ".pre-commit-config.yaml",
4
- "makefile",
5
- "pyproject.toml",
6
3
  ".env.dist",
7
- "README.md",
8
4
  ".gitignore",
5
+ ".pre-commit-config.yaml",
9
6
  "CLAUDE.md",
10
- "x_auth/enums.py",
7
+ "README.md",
8
+ "makefile",
9
+ "pyproject.toml",
10
+ "tests/test_proxy.py",
11
11
  "x_auth/controller.py",
12
- "x_auth/models.py",
13
- "x_auth/types.py",
12
+ "x_auth/enums.py",
14
13
  "x_auth/exceptions.py",
15
- "x_auth/middleware.py"
14
+ "x_auth/middleware.py",
15
+ "x_auth/models.py",
16
+ "x_auth/types.py"
16
17
  ]
17
18
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.2.67",
3
+ "distance": 1,
4
+ "node": "gdbf4963c34ccf29752f04494ca9dfb7e368700c2",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-08-17"
8
+ }
xn_auth-0.2.66/.env.dist DELETED
@@ -1 +0,0 @@
1
- VENV=venv
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.2.66",
3
- "distance": 0,
4
- "node": "gedb47eec341e11cdbc3c44441cc795f50c552a42",
5
- "dirty": false,
6
- "branch": "main",
7
- "node_date": "2026-07-19"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes