asockslib 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,72 @@
1
+ """Pydantic models for ASocks API v2 requests and responses.
2
+
3
+ Covers all 25 endpoints documented at https://docs.asocks.com/en/.
4
+
5
+ Modules:
6
+ - :mod:`.enums` — port statuses and connection/proxy type identifiers.
7
+ - :mod:`.directory` — countries, states, cities, ASN entries.
8
+ - :mod:`.port` — proxy port models and list responses.
9
+ - :mod:`.requests` — bodies and query parameters for mutations.
10
+ - :mod:`.responses` — typed wrappers for API responses.
11
+
12
+ Every model uses ``extra="allow"`` so that new fields added by the API
13
+ are accepted without breaking existing code.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from asockslib.models.directory import (
19
+ ASNInfo,
20
+ ASNListResponse,
21
+ CityInfo,
22
+ CountryInfo,
23
+ StateInfo,
24
+ )
25
+ from asockslib.models.enums import (
26
+ AuthType,
27
+ ConnectionType,
28
+ ConnectionTypeId,
29
+ PortStatus,
30
+ ProxyType,
31
+ ProxyTypeId,
32
+ ServerPortType,
33
+ )
34
+ from asockslib.models.port import (
35
+ PortInfo,
36
+ PortListResponse,
37
+ )
38
+ from asockslib.models.requests import (
39
+ CreatePortRequest,
40
+ CreateTemplateRequest,
41
+ PortFilterParams,
42
+ UpdatePortRequest,
43
+ UpdateTemplateRequest,
44
+ WhitelistAddRequest,
45
+ )
46
+ from asockslib.models.responses import (
47
+ BalanceResponse,
48
+ )
49
+
50
+ __all__ = [
51
+ "ASNInfo",
52
+ "ASNListResponse",
53
+ "AuthType",
54
+ "BalanceResponse",
55
+ "CityInfo",
56
+ "ConnectionType",
57
+ "ConnectionTypeId",
58
+ "CountryInfo",
59
+ "CreatePortRequest",
60
+ "CreateTemplateRequest",
61
+ "PortFilterParams",
62
+ "PortInfo",
63
+ "PortListResponse",
64
+ "PortStatus",
65
+ "ProxyType",
66
+ "ProxyTypeId",
67
+ "ServerPortType",
68
+ "StateInfo",
69
+ "UpdatePortRequest",
70
+ "UpdateTemplateRequest",
71
+ "WhitelistAddRequest",
72
+ ]
@@ -0,0 +1,65 @@
1
+ """Directory (geo) models for the ASocks API v2.
2
+
3
+ Country, state, city, and ASN data structures returned by the
4
+ ``/v2/dir/*`` endpoints.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pydantic import AliasChoices, BaseModel, Field
10
+
11
+
12
+ class CountryInfo(BaseModel, extra="allow"):
13
+ """Country entry from ``GET /v2/dir/countries``."""
14
+
15
+ id: int = Field(description="Internal country ID")
16
+ name: str = Field(default="", description="Full country name")
17
+ code: str = Field(
18
+ default="",
19
+ description="ISO country code (e.g. US, DE)",
20
+ validation_alias=AliasChoices("code", "short_name"),
21
+ )
22
+
23
+ @property
24
+ def short_name(self) -> str:
25
+ """Alias for :pyattr:`code` (backward-compatible)."""
26
+ return self.code
27
+
28
+
29
+ class StateInfo(BaseModel, extra="allow"):
30
+ """State/region entry from ``GET /v2/dir/states``."""
31
+
32
+ id: int = Field(description="Internal state ID")
33
+ name: str = Field(default="", description="State/region name")
34
+ dir_country_id: int | None = Field(default=None, description="Parent country ID")
35
+
36
+
37
+ class CityInfo(BaseModel, extra="allow"):
38
+ """City entry from ``GET /v2/dir/cities``."""
39
+
40
+ id: int = Field(description="Internal city ID")
41
+ name: str = Field(default="", description="City name")
42
+ dir_country_id: int | None = Field(default=None, description="Parent country ID")
43
+ dir_state_id: int | None = Field(default=None, description="Parent state ID")
44
+
45
+
46
+ class ASNInfo(BaseModel, extra="allow"):
47
+ """ASN (Autonomous System Number) entry from ``GET /v2/dir/asns``."""
48
+
49
+ asn: int = Field(description="Autonomous System Number")
50
+ name: str = Field(default="", description="ISP / provider name")
51
+
52
+
53
+ class ASNListResponse(BaseModel, extra="allow"):
54
+ """Paginated response for ``GET /v2/dir/asns``."""
55
+
56
+ data: list[ASNInfo] = Field(default_factory=list[ASNInfo], description="ASN entries")
57
+ current_page: int = Field(default=1, description="Current page number")
58
+ per_page: int = Field(default=1000, description="Items per page")
59
+ total: int = Field(default=0, description="Total number of ASN entries")
60
+ last_page: int = Field(default=1, description="Last page number")
61
+
62
+ @property
63
+ def items(self) -> list[ASNInfo]:
64
+ """Alias for ``data``."""
65
+ return self.data
@@ -0,0 +1,65 @@
1
+ """Enum types for the ASocks API v2.
2
+
3
+ Centralizes all enumeration types used across the library:
4
+ port statuses, proxy types, connection types, and identifiers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import IntEnum, StrEnum
10
+
11
+
12
+ class PortStatus(IntEnum):
13
+ """Port status codes used by the ASocks API."""
14
+
15
+ ACTIVE = 1
16
+ INACTIVE = 0
17
+ EXPIRED = 2
18
+
19
+
20
+ class ProxyType(StrEnum):
21
+ """Proxy category."""
22
+
23
+ RESIDENTIAL = "residential"
24
+ MOBILE = "mobile"
25
+ CORPORATE = "corporate"
26
+
27
+
28
+ class ConnectionType(StrEnum):
29
+ """Connection mode for a proxy port."""
30
+
31
+ KEEP_PROXY = "keep-proxy"
32
+ KEEP_CONNECTION = "keep-connection"
33
+ ROTATE_CONNECTION = "rotate-connection"
34
+ KEEP_CONNECTION_LOW_TRUST = "keep-connection-low-trust"
35
+
36
+
37
+ class AuthType(StrEnum):
38
+ """Authentication method."""
39
+
40
+ LOGIN_PASSWORD = "login-and-password"
41
+ IP_WHITELIST = "ip-whitelist"
42
+
43
+
44
+ class ProxyTypeId(IntEnum):
45
+ """Numeric proxy-type identifiers for API requests."""
46
+
47
+ RESIDENTIAL = 1
48
+ ALL = 2
49
+ MOBILE = 3
50
+ CORPORATE = 4
51
+
52
+
53
+ class ConnectionTypeId(IntEnum):
54
+ """Numeric connection-type identifiers for API requests."""
55
+
56
+ KEEP_PROXY = 1
57
+ KEEP_CONNECTION = 2
58
+ ROTATE_CONNECTION = 3
59
+
60
+
61
+ class ServerPortType(IntEnum):
62
+ """Server port type."""
63
+
64
+ SHARED = 0
65
+ DEDICATED = 1
@@ -0,0 +1,187 @@
1
+ """Port models for the ASocks API v2.
2
+
3
+ Defines :class:`PortInfo` (single proxy port) and
4
+ :class:`PortListResponse` (paginated port list wrapper).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import ClassVar, cast
10
+
11
+ from beartype import beartype
12
+ from pydantic import AliasChoices, BaseModel, Field, field_validator, model_validator
13
+
14
+ from asockslib.models.enums import PortStatus
15
+
16
+
17
+ class PortInfo(BaseModel, extra="allow"):
18
+ """A single proxy port returned by the API.
19
+
20
+ Produced by ``GET /v2/proxy/ports``, ``GET /v2/proxy/port-info``
21
+ and ``POST /v2/proxy/create-port``.
22
+
23
+ Attributes:
24
+ id: Unique port identifier.
25
+ host: Proxy server address.
26
+ port: Port number.
27
+ login: Authentication login.
28
+ password: Authentication password.
29
+ protocol: Proxy protocol (``socks5``, ``http``, …).
30
+ proxy_type: Proxy category.
31
+ country_code: ISO country code.
32
+ status: Port status code (see :class:`PortStatus`).
33
+
34
+ Example::
35
+
36
+ print(port.proxy_url) # socks5://user:pass@host:10001
37
+ print(port.is_active) # True
38
+ """
39
+
40
+ id: int = Field(description="Unique port identifier")
41
+ host: str = Field(
42
+ default="",
43
+ description="Proxy host address",
44
+ validation_alias=AliasChoices("host", "server"),
45
+ )
46
+ port: int = Field(default=0, description="Proxy port number")
47
+ login: str = Field(default="", description="Authentication login")
48
+ password: str = Field(default="", description="Authentication password")
49
+ protocol: str = Field(default="socks5", description="Proxy protocol")
50
+ proxy_type: str | None = Field(default=None, description="Proxy type category")
51
+ country: str = Field(default="", description="Country name")
52
+ country_code: str = Field(default="", description="ISO country code")
53
+ city: str = Field(default="", description="City name")
54
+ state: str = Field(default="", description="State name")
55
+ status: int | str = Field(default=1, description="Port status code")
56
+ name: str = Field(default="", description="User-defined port name")
57
+ asn: int | None = Field(default=None, description="Autonomous System Number")
58
+ external_ip: str = Field(default="", description="External IP address")
59
+
60
+ @model_validator(mode="before")
61
+ @classmethod
62
+ def _coerce_none_strings(cls, data: dict[str, object] | object) -> dict[str, object] | object:
63
+ """Coerce ``None`` values to empty strings for str fields."""
64
+ if not isinstance(data, dict):
65
+ return data
66
+ data = cast("dict[str, object]", data)
67
+ _str_fields = (
68
+ "host",
69
+ "login",
70
+ "password",
71
+ "protocol",
72
+ "country",
73
+ "country_code",
74
+ "city",
75
+ "state",
76
+ "name",
77
+ "external_ip",
78
+ )
79
+ for fld in _str_fields:
80
+ if fld in data and data[fld] is None:
81
+ data[fld] = ""
82
+ return data
83
+
84
+ expires_at: str | None = Field(default=None, description="Expiration timestamp")
85
+ created_at: str | None = Field(default=None, description="Creation timestamp")
86
+ ttl: int | None = Field(default=None, description="Time-to-live in days")
87
+ traffic_limit: float | None = Field(default=None, description="Traffic limit in GB")
88
+ traffic_used: float | None = Field(default=None, description="Traffic used in GB")
89
+ type_id: int | None = Field(default=None, description="Connection type ID")
90
+ proxy_type_id: int | None = Field(default=None, description="Proxy type ID")
91
+ server_port_type_id: int | None = Field(default=None, description="Server port type ID")
92
+
93
+ _STATUS_MAP: ClassVar[dict[str, int]] = {
94
+ "active": 1,
95
+ "inactive": 0,
96
+ "expired": 2,
97
+ "stopped": 0,
98
+ "error": 0,
99
+ }
100
+
101
+ @field_validator("status", mode="before")
102
+ @classmethod
103
+ def _coerce_status(cls, v: object) -> int:
104
+ """Accept both int and string status from the API.
105
+
106
+ Typed ``object`` (not ``int | str``): a ``mode="before"`` validator
107
+ receives the raw, not-yet-validated JSON value, which may be any type.
108
+ """
109
+ if isinstance(v, int):
110
+ return v
111
+ if isinstance(v, str):
112
+ low = v.strip().lower()
113
+ if low.isdigit():
114
+ return int(low)
115
+ return cls._STATUS_MAP.get(low, 0)
116
+ return int(cast("int", v))
117
+
118
+ @property
119
+ def proxy_url(self) -> str:
120
+ """Full proxy URL (``protocol://login:password@host:port``)."""
121
+ auth = f"{self.login}:{self.password}@" if self.login else ""
122
+ proto = self.protocol or "socks5"
123
+ return f"{proto}://{auth}{self.host}:{self.port}"
124
+
125
+ @beartype
126
+ def format_with_template(self, template: str) -> str:
127
+ """Format the proxy using a template string.
128
+
129
+ Supported placeholders: ``{protocol}``, ``{id}``, ``{login}``,
130
+ ``{password}``, ``{ip}``, ``{port}``, ``{refresh_link}``,
131
+ ``{name}``, ``{external_ip}``.
132
+
133
+ Example::
134
+
135
+ port.format_with_template("{ip}:{port}:{login}:{password}")
136
+ # "1.2.3.4:8080:u:p"
137
+ """
138
+ refresh = f"https://api.asocks.com/v2/proxy/refresh-ip/{self.id}"
139
+ return (
140
+ template.replace("{protocol}", self.protocol or "socks5")
141
+ .replace("{id}", str(self.id))
142
+ .replace("{login}", self.login)
143
+ .replace("{password}", self.password)
144
+ .replace("{ip}", self.host)
145
+ .replace("{port}", str(self.port))
146
+ .replace("{refresh_link}", refresh)
147
+ .replace("{name}", self.name)
148
+ .replace("{external_ip}", self.external_ip)
149
+ )
150
+
151
+ @property
152
+ def is_active(self) -> bool:
153
+ """``True`` when the port status is :attr:`PortStatus.ACTIVE`."""
154
+ return self.status == PortStatus.ACTIVE # type: ignore[comparison-overlap]
155
+
156
+
157
+ class PortListResponse(BaseModel, extra="allow"):
158
+ """Response wrapper for ``GET /v2/proxy/ports``.
159
+
160
+ The API may return either a flat list or a paginated envelope
161
+ (``{data: [...], total: N, ...}``); this model handles both.
162
+ """
163
+
164
+ success: bool = Field(default=True)
165
+ message: list[PortInfo] | dict[str, object] = Field(default_factory=list[PortInfo])
166
+
167
+ @property
168
+ def items(self) -> list[PortInfo]:
169
+ """Port list regardless of the envelope format."""
170
+ if isinstance(self.message, list):
171
+ return self.message
172
+ data = self.message.get("data", [])
173
+ if isinstance(data, list):
174
+ return [PortInfo.model_validate(d) for d in cast("list[object]", data)]
175
+ return []
176
+
177
+ @property
178
+ def total(self) -> int:
179
+ """Total number of ports."""
180
+ if isinstance(self.message, dict):
181
+ raw_total = self.message.get("total", 0)
182
+ if isinstance(raw_total, int):
183
+ return raw_total
184
+ if isinstance(raw_total, str) and raw_total.strip().lstrip("-").isdigit():
185
+ return int(raw_total)
186
+ return 0
187
+ return len(self.message)
@@ -0,0 +1,89 @@
1
+ """Request models for the ASocks API v2.
2
+
3
+ Bodies and query parameters for mutations: port creation, updates,
4
+ template management, and whitelist operations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class CreatePortRequest(BaseModel):
13
+ """Body for ``POST /v2/proxy/create-port``.
14
+
15
+ Example::
16
+
17
+ req = CreatePortRequest(country_code="US", count=5, ttl=7)
18
+ """
19
+
20
+ country_code: str = Field(default="", description="ISO country code (e.g. US)")
21
+ state: str = Field(default="", description="State name")
22
+ city: str = Field(default="", description="City name")
23
+ asn: int | None = Field(default=None, description="Autonomous System Number")
24
+ type_id: int = Field(default=1, description="Connection type ID")
25
+ proxy_type_id: int = Field(default=1, description="Proxy type ID")
26
+ name: str = Field(default="", description="Port display name")
27
+ server_port_type_id: int = Field(
28
+ default=0,
29
+ description="Server port type (0=shared, 1=dedicated)",
30
+ )
31
+ count: int = Field(default=1, ge=1, le=1000, description="Number of ports to create")
32
+ ttl: int = Field(default=1, ge=1, description="Time-to-live in days")
33
+ traffic_limit: int = Field(default=10, ge=1, description="Traffic limit in GB")
34
+
35
+
36
+ class PortFilterParams(BaseModel):
37
+ """Query parameters for ``GET /v2/proxy/ports``.
38
+
39
+ All fields are optional; omitted filters return all ports.
40
+ """
41
+
42
+ id: int | None = Field(default=None, description="Filter by port ID")
43
+ proxy: str | None = Field(default=None, description="Filter by proxy address")
44
+ # Field names mirror the ASocks API's camelCase query params exactly
45
+ # (sent verbatim by ASocksClient.list_ports) — not a style violation.
46
+ countryName: str | None = Field(default=None, description="Filter by country name") # noqa: N815
47
+ stateName: str | None = Field(default=None, description="Filter by state name") # noqa: N815
48
+ cityName: str | None = Field(default=None, description="Filter by city name") # noqa: N815
49
+ asn: int | None = Field(default=None, description="Filter by ASN")
50
+ status: int | None = Field(default=None, description="Filter by status")
51
+ template_id: int | None = Field(default=None, description="Filter by template ID")
52
+ page: int = Field(default=1, ge=1)
53
+ per_page: int = Field(default=50, ge=1, le=200)
54
+
55
+
56
+ class UpdatePortRequest(BaseModel):
57
+ """Body for ``PATCH /v2/proxy/update-port/{id}``."""
58
+
59
+ geo_country_ids: list[int] | None = Field(default=None, description="Country IDs")
60
+ geo_state_id: int | None = Field(default=None, description="State ID")
61
+ geo_city_id: int | None = Field(default=None, description="City ID")
62
+ asns: list[int] | None = Field(default=None, description="ASN list")
63
+ connection_type: str | None = Field(default=None, description="Connection type")
64
+ auth_type: str | None = Field(default=None, description="Auth type")
65
+ proxy_types: list[str] | None = Field(default=None, description="Proxy type list")
66
+ name: str | None = Field(default=None, description="Port name")
67
+ ttl: int | None = Field(default=None, description="TTL in days")
68
+ traffic_limit: int | None = Field(default=None, description="Traffic limit in GB")
69
+
70
+
71
+ class CreateTemplateRequest(BaseModel):
72
+ """Body for ``POST /v2/proxy-template/create-template``."""
73
+
74
+ label: str = Field(description="Template label")
75
+ template: str = Field(description="Template pattern, e.g. {ip}:{port}")
76
+
77
+
78
+ class UpdateTemplateRequest(BaseModel):
79
+ """Body for ``PATCH /v2/proxy-template/update-template``."""
80
+
81
+ label: str | None = Field(default=None, description="Template label")
82
+ template: str | None = Field(default=None, description="Template pattern")
83
+
84
+
85
+ class WhitelistAddRequest(BaseModel):
86
+ """Body for ``POST /v2/whitelist/add``."""
87
+
88
+ ip: str = Field(description="IP address to whitelist")
89
+ description: str = Field(default="", description="Description for the IP entry")
@@ -0,0 +1,20 @@
1
+ """Response models for the ASocks API v2.
2
+
3
+ Typed wrappers for API response payloads that don't fit into
4
+ the port or directory categories.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class BalanceResponse(BaseModel, extra="allow"):
13
+ """Response for ``GET /v2/user/balance``."""
14
+
15
+ success: bool = Field(default=True)
16
+ balance: float = Field(default=0, description="Account balance in USD")
17
+ balance_traffic: float = Field(default=0, description="Traffic balance")
18
+ all_available_traffic: float = Field(default=0, description="Total available traffic")
19
+ prepared_traffic_balance: float = Field(default=0, description="Prepared traffic balance")
20
+ balance_hold: float = Field(default=0, description="Held balance")