python-mytnb 0.2.0__tar.gz → 0.4.0__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 (26) hide show
  1. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/PKG-INFO +20 -9
  2. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/README.md +13 -5
  3. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/pyproject.toml +8 -4
  4. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/__init__.py +4 -0
  5. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/client/client.py +7 -7
  6. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/client/legacy.py +4 -4
  7. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/models.py +36 -0
  8. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/tests/test_client.py +1 -1
  9. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/tests/test_models.py +90 -0
  10. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/uv.lock +36 -12
  11. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/.github/workflows/ci.yml +0 -0
  12. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/.github/workflows/publish.yml +0 -0
  13. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/.gitignore +0 -0
  14. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/LICENSE +0 -0
  15. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/__main__.py +0 -0
  16. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/auth.py +0 -0
  17. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/cli.py +0 -0
  18. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/client/__init__.py +0 -0
  19. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/client/auth.py +0 -0
  20. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/client/config.py +0 -0
  21. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/client/rest.py +0 -0
  22. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/crypto.py +0 -0
  23. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/src/mytnb/exceptions.py +0 -0
  24. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/tests/test_auth.py +0 -0
  25. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/tests/test_cli.py +0 -0
  26. {python_mytnb-0.2.0 → python_mytnb-0.4.0}/tests/test_crypto.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-mytnb
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Python library to interface with the myTNB API (Tenaga Nasional Berhad)
5
5
  Project-URL: Repository, https://github.com/danieyal/python-mytnb
6
6
  License-Expression: MIT
@@ -16,16 +16,19 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Topic :: Software Development :: Libraries
18
18
  Requires-Python: >=3.10
19
- Requires-Dist: click>=8.3.3
20
19
  Requires-Dist: cryptography>=42.0
20
+ Requires-Dist: curl-cffi>=0.7
21
21
  Requires-Dist: httpx>=0.27
22
22
  Requires-Dist: pydantic>=2.0
23
- Requires-Dist: rich>=15.0.0
24
- Requires-Dist: tls-client>=1.0
23
+ Provides-Extra: cli
24
+ Requires-Dist: click>=8.3.3; extra == 'cli'
25
+ Requires-Dist: rich>=15.0.0; extra == 'cli'
25
26
  Provides-Extra: dev
27
+ Requires-Dist: click>=8.3.3; extra == 'dev'
26
28
  Requires-Dist: pylint>=3.0; extra == 'dev'
27
29
  Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
28
30
  Requires-Dist: pytest>=7.0; extra == 'dev'
31
+ Requires-Dist: rich>=15.0.0; extra == 'dev'
29
32
  Requires-Dist: ruff>=0.6; extra == 'dev'
30
33
  Description-Content-Type: text/markdown
31
34
 
@@ -79,6 +82,14 @@ asyncio.run(main())
79
82
 
80
83
  ## CLI
81
84
 
85
+ The `mytnb` command-line tool requires the optional `cli` extra (it pulls in
86
+ `click` and `rich`, which the library itself does not need). If you installed
87
+ without it, `mytnb` (or `python -m mytnb`) will fail to start:
88
+
89
+ ```bash
90
+ pip install "python-mytnb[cli]"
91
+ ```
92
+
82
93
  Pass credentials directly:
83
94
 
84
95
  ```bash
@@ -119,11 +130,11 @@ mytnb init-config # Generate a starter config file
119
130
 
120
131
  myTNB uses two API backends, both handled transparently by this library:
121
132
 
122
- | Backend | Domain | Auth | Used for |
123
- | ----------- | -------------------------- | ------------------------------------------- | ----------------------------------- |
124
- | REST | `api.mytnb.com.my` | JWT + API key | Bill eligibility, eligibility icons |
125
- | AWS Gateway | `api.mytnb.com.my/core/api`| Encrypted payloads (AES-256-CBC + RSA-OAEP) | Account listing (auto-discovery) |
126
- | Legacy ASMX | `mytnbapp.tnb.com.my` | Encrypted payloads (AES-256-CBC + RSA-OAEP) | Usage data, billing, services |
133
+ | Backend | Domain | Auth | Used for |
134
+ | ----------- | --------------------------- | ------------------------------------------- | ----------------------------------- |
135
+ | REST | `api.mytnb.com.my` | JWT + API key | Bill eligibility, eligibility icons |
136
+ | AWS Gateway | `api.mytnb.com.my/core/api` | Encrypted payloads (AES-256-CBC + RSA-OAEP) | Account listing (auto-discovery) |
137
+ | Legacy ASMX | `mytnbapp.tnb.com.my` | Encrypted payloads (AES-256-CBC + RSA-OAEP) | Usage data, billing, services |
127
138
 
128
139
  Request encryption for the ASMX API is automatic — just pass plaintext parameters.
129
140
 
@@ -48,6 +48,14 @@ asyncio.run(main())
48
48
 
49
49
  ## CLI
50
50
 
51
+ The `mytnb` command-line tool requires the optional `cli` extra (it pulls in
52
+ `click` and `rich`, which the library itself does not need). If you installed
53
+ without it, `mytnb` (or `python -m mytnb`) will fail to start:
54
+
55
+ ```bash
56
+ pip install "python-mytnb[cli]"
57
+ ```
58
+
51
59
  Pass credentials directly:
52
60
 
53
61
  ```bash
@@ -88,11 +96,11 @@ mytnb init-config # Generate a starter config file
88
96
 
89
97
  myTNB uses two API backends, both handled transparently by this library:
90
98
 
91
- | Backend | Domain | Auth | Used for |
92
- | ----------- | -------------------------- | ------------------------------------------- | ----------------------------------- |
93
- | REST | `api.mytnb.com.my` | JWT + API key | Bill eligibility, eligibility icons |
94
- | AWS Gateway | `api.mytnb.com.my/core/api`| Encrypted payloads (AES-256-CBC + RSA-OAEP) | Account listing (auto-discovery) |
95
- | Legacy ASMX | `mytnbapp.tnb.com.my` | Encrypted payloads (AES-256-CBC + RSA-OAEP) | Usage data, billing, services |
99
+ | Backend | Domain | Auth | Used for |
100
+ | ----------- | --------------------------- | ------------------------------------------- | ----------------------------------- |
101
+ | REST | `api.mytnb.com.my` | JWT + API key | Bill eligibility, eligibility icons |
102
+ | AWS Gateway | `api.mytnb.com.my/core/api` | Encrypted payloads (AES-256-CBC + RSA-OAEP) | Account listing (auto-discovery) |
103
+ | Legacy ASMX | `mytnbapp.tnb.com.my` | Encrypted payloads (AES-256-CBC + RSA-OAEP) | Usage data, billing, services |
96
104
 
97
105
  Request encryption for the ASMX API is automatic — just pass plaintext parameters.
98
106
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-mytnb"
7
- version = "0.2.0"
7
+ version = "0.4.0"
8
8
  description = "Python library to interface with the myTNB API (Tenaga Nasional Berhad)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -25,17 +25,21 @@ dependencies = [
25
25
  "httpx>=0.27",
26
26
  "pydantic>=2.0",
27
27
  "cryptography>=42.0",
28
- "tls_client>=1.0",
29
- "click>=8.3.3",
30
- "rich>=15.0.0",
28
+ "curl_cffi>=0.7",
31
29
  ]
32
30
 
33
31
  [project.optional-dependencies]
32
+ cli = [
33
+ "click>=8.3.3",
34
+ "rich>=15.0.0",
35
+ ]
34
36
  dev = [
35
37
  "pytest>=7.0",
36
38
  "pytest-asyncio>=0.21",
37
39
  "pylint>=3.0",
38
40
  "ruff>=0.6",
41
+ "click>=8.3.3",
42
+ "rich>=15.0.0",
39
43
  ]
40
44
 
41
45
  [project.scripts]
@@ -10,6 +10,8 @@ from mytnb.models import (
10
10
  DailyUsage,
11
11
  Metric,
12
12
  TariffBlock,
13
+ TariffBlockLegendGroup,
14
+ TariffBlockLegendItem,
13
15
  UsageMetric,
14
16
  )
15
17
 
@@ -24,5 +26,7 @@ __all__ = [
24
26
  "DailyUsage",
25
27
  "Metric",
26
28
  "TariffBlock",
29
+ "TariffBlockLegendGroup",
30
+ "TariffBlockLegendItem",
27
31
  "UsageMetric",
28
32
  ]
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from typing import Any, Optional
6
6
 
7
7
  import httpx
8
- import tls_client
8
+ from curl_cffi import requests as curl_requests
9
9
 
10
10
  from mytnb.auth import Credentials
11
11
  from mytnb.client.auth import login
@@ -40,7 +40,7 @@ class MyTNBClient:
40
40
  self._timeout = timeout
41
41
  self._use_staging_key = use_staging_key
42
42
  self._http_client: Optional[httpx.AsyncClient] = None
43
- self._tls_session: Optional[tls_client.Session] = None
43
+ self._tls_session: Optional[curl_requests.Session] = None
44
44
  self._rest: Optional[_RestTransport] = None
45
45
  self._legacy: Optional[_LegacyTransport] = None
46
46
 
@@ -59,14 +59,14 @@ class MyTNBClient:
59
59
  return self._http_client
60
60
 
61
61
  @property
62
- def _legacy_client(self) -> tls_client.Session:
63
- """Get or create a tls_client session for legacy ASMX requests.
62
+ def _legacy_client(self) -> curl_requests.Session:
63
+ """Get or create a curl_cffi session for legacy ASMX requests.
64
64
 
65
- Uses an Android TLS fingerprint to bypass CloudFront WAF.
65
+ Uses a TLS fingerprint to bypass CloudFront WAF.
66
66
  """
67
67
  if self._tls_session is None:
68
- self._tls_session = tls_client.Session(
69
- client_identifier="okhttp4_android_13",
68
+ self._tls_session = curl_requests.Session(
69
+ impersonate="chrome131",
70
70
  )
71
71
  return self._tls_session
72
72
 
@@ -7,7 +7,7 @@ import json
7
7
  import logging
8
8
  from typing import Any
9
9
 
10
- import tls_client
10
+ from curl_cffi import requests as curl_requests
11
11
 
12
12
  from mytnb.auth import Credentials
13
13
  from mytnb.client.config import (
@@ -30,7 +30,7 @@ class _LegacyTransport:
30
30
 
31
31
  def __init__(
32
32
  self,
33
- session: tls_client.Session,
33
+ session: curl_requests.Session,
34
34
  credentials: Credentials,
35
35
  timeout: float,
36
36
  use_staging_key: bool,
@@ -85,7 +85,7 @@ class _LegacyTransport:
85
85
  """Make a POST request to the legacy ASMX API.
86
86
 
87
87
  Automatically encrypts the data using AES-256-CBC + RSA-OAEP.
88
- Uses tls_client with an Android TLS fingerprint to bypass CloudFront WAF.
88
+ Uses curl_cffi with a TLS fingerprint to bypass CloudFront WAF.
89
89
  """
90
90
  url = f"{LEGACY_BASE_URL}/{endpoint}"
91
91
  req_headers = self.headers()
@@ -99,7 +99,7 @@ class _LegacyTransport:
99
99
  url,
100
100
  headers=req_headers,
101
101
  json=body,
102
- timeout_seconds=int(self._timeout),
102
+ timeout=int(self._timeout),
103
103
  )
104
104
  logger.debug("Legacy POST %s → %s", endpoint, response.status_code)
105
105
 
@@ -150,6 +150,26 @@ class ByMonthData(BaseModel):
150
150
  model_config = {"populate_by_name": True}
151
151
 
152
152
 
153
+ class TariffBlockLegendItem(BaseModel):
154
+ """A single tariff rate block in the legend (e.g. BLK1: RM 0.218/kWh)."""
155
+
156
+ block_id: str = Field(alias="BlockId")
157
+ block_range: str = Field(default="", alias="BlockRange")
158
+ block_price: str = Field(default="", alias="BlockPrice")
159
+
160
+ model_config = {"populate_by_name": True, "extra": "ignore"}
161
+
162
+
163
+ class TariffBlockLegendGroup(BaseModel):
164
+ """A monthly group of RP4 tariff legend items."""
165
+
166
+ items: list[TariffBlockLegendItem] = Field(
167
+ default_factory=list, alias="TariffBlocksLegend"
168
+ )
169
+
170
+ model_config = {"populate_by_name": True, "extra": "ignore"}
171
+
172
+
153
173
  class AccountUsage(BaseModel):
154
174
  """Full account usage response from GetAccountUsageSmart."""
155
175
 
@@ -161,6 +181,8 @@ class AccountUsage(BaseModel):
161
181
  start_date: Optional[str] = None
162
182
  end_date: Optional[str] = None
163
183
  date_range: Optional[str] = None
184
+ tariff_blocks_legend: list[TariffBlockLegendItem] = Field(default_factory=list)
185
+ tariff_blocks_legend_rp4: list[TariffBlockLegendGroup] = Field(default_factory=list)
164
186
 
165
187
  @classmethod
166
188
  def from_api_response(cls, data: dict) -> "AccountUsage":
@@ -178,6 +200,18 @@ class AccountUsage(BaseModel):
178
200
  by_day_raw = data.get("ByDay", [])
179
201
  by_day = [DailyUsageWeek.model_validate(w) for w in by_day_raw]
180
202
 
203
+ # Tariff block legends (residential)
204
+ legend_raw = data.get("TariffBlocksLegend", [])
205
+ tariff_blocks_legend = [
206
+ TariffBlockLegendItem.model_validate(item) for item in legend_raw
207
+ ]
208
+
209
+ # Tariff block legends by month (RP4 commercial)
210
+ rp4_raw = data.get("TariffBlocksLegendByMonthListRP4", [])
211
+ tariff_blocks_legend_rp4 = [
212
+ TariffBlockLegendGroup.model_validate(group) for group in rp4_raw
213
+ ]
214
+
181
215
  return cls(
182
216
  usage_metrics=usage_metrics,
183
217
  cost_metrics=cost_metrics,
@@ -187,6 +221,8 @@ class AccountUsage(BaseModel):
187
221
  start_date=data.get("StartDate"),
188
222
  end_date=data.get("EndDate"),
189
223
  date_range=data.get("DateRange"),
224
+ tariff_blocks_legend=tariff_blocks_legend,
225
+ tariff_blocks_legend_rp4=tariff_blocks_legend_rp4,
190
226
  )
191
227
 
192
228
  @property
@@ -38,7 +38,7 @@ def _mock_response(data: dict, status_code: int = 200) -> httpx.Response:
38
38
 
39
39
 
40
40
  def _mock_tls_response(data: dict, status_code: int = 200) -> MagicMock:
41
- """Create a mock tls_client response."""
41
+ """Create a mock curl_cffi response."""
42
42
  resp = MagicMock()
43
43
  resp.status_code = status_code
44
44
  resp.json.return_value = data
@@ -12,6 +12,8 @@ from mytnb.models import (
12
12
  MonthlyTariffBlock,
13
13
  SMRAccount,
14
14
  TariffBlock,
15
+ TariffBlockLegendGroup,
16
+ TariffBlockLegendItem,
15
17
  )
16
18
 
17
19
  # ── Sample API response data ─────────────────────────────────────────────
@@ -115,6 +117,36 @@ FULL_API_RESPONSE = {
115
117
  "StartDate": "2026-05-01",
116
118
  "EndDate": "2026-05-15",
117
119
  "DateRange": "1 May - 15 May 2026",
120
+ "TariffBlocksLegend": [
121
+ {
122
+ "BlockId": "BLK1",
123
+ "RGB": {"R": 102, "G": 196, "B": 183},
124
+ "BlockRange": "1 - 200 kWh",
125
+ "BlockPrice": "RM 0.218 / kWh",
126
+ },
127
+ {
128
+ "BlockId": "BLK2",
129
+ "RGB": {"R": 158, "G": 214, "B": 182},
130
+ "BlockRange": "201 - 300 kWh",
131
+ "BlockPrice": "RM 0.334 / kWh",
132
+ },
133
+ ],
134
+ "TariffBlocksLegendByMonthListRP4": [
135
+ {
136
+ "TariffBlocksLegend": [
137
+ {
138
+ "BlockId": "EnergyCharge",
139
+ "BlockRange": "0",
140
+ "BlockPrice": "RM 0.2703",
141
+ },
142
+ {
143
+ "BlockId": "CustomerCharge",
144
+ "BlockRange": "0",
145
+ "BlockPrice": "RM 10.00",
146
+ },
147
+ ],
148
+ },
149
+ ],
118
150
  }
119
151
 
120
152
 
@@ -227,6 +259,20 @@ class TestAccountUsage:
227
259
  assert len(usage.by_day) == 1
228
260
  assert len(usage.by_day[0].days) == 1
229
261
 
262
+ def test_tariff_blocks_legend(self):
263
+ usage = AccountUsage.from_api_response(FULL_API_RESPONSE)
264
+ assert len(usage.tariff_blocks_legend) == 2
265
+ assert usage.tariff_blocks_legend[0].block_id == "BLK1"
266
+ assert usage.tariff_blocks_legend[0].block_range == "1 - 200 kWh"
267
+ assert usage.tariff_blocks_legend[0].block_price == "RM 0.218 / kWh"
268
+
269
+ def test_tariff_blocks_legend_rp4(self):
270
+ usage = AccountUsage.from_api_response(FULL_API_RESPONSE)
271
+ assert len(usage.tariff_blocks_legend_rp4) == 1
272
+ assert len(usage.tariff_blocks_legend_rp4[0].items) == 2
273
+ assert usage.tariff_blocks_legend_rp4[0].items[0].block_id == "EnergyCharge"
274
+ assert usage.tariff_blocks_legend_rp4[0].items[0].block_price == "RM 0.2703"
275
+
230
276
  def test_empty_response(self):
231
277
  usage = AccountUsage.from_api_response({})
232
278
  assert usage.current_usage_kwh is None
@@ -421,3 +467,47 @@ class TestCustomerAccount:
421
467
  assert acc.account_desc == ""
422
468
  assert acc.owner_name == ""
423
469
  assert acc.account_st_address == ""
470
+
471
+
472
+ class TestTariffBlockLegendItem:
473
+ """Tests for TariffBlockLegendItem (residential tariff rate block)."""
474
+
475
+ def test_parse(self):
476
+ item = TariffBlockLegendItem.model_validate({
477
+ "BlockId": "BLK1",
478
+ "RGB": {"R": 102, "G": 196, "B": 183},
479
+ "BlockRange": "1 - 200 kWh",
480
+ "BlockPrice": "RM 0.218 / kWh",
481
+ })
482
+ assert item.block_id == "BLK1"
483
+ assert item.block_range == "1 - 200 kWh"
484
+ assert item.block_price == "RM 0.218 / kWh"
485
+
486
+ def test_ignores_extra_fields(self):
487
+ item = TariffBlockLegendItem.model_validate({
488
+ "BlockId": "BLK1",
489
+ "RGB": {"R": 0, "G": 0, "B": 0},
490
+ "BlockRange": "",
491
+ "BlockPrice": "",
492
+ "Month": "Jan",
493
+ })
494
+ assert item.block_id == "BLK1"
495
+
496
+
497
+ class TestTariffBlockLegendGroup:
498
+ """Tests for TariffBlockLegendGroup (RP4 monthly tariff breakdown)."""
499
+
500
+ def test_parse(self):
501
+ group = TariffBlockLegendGroup.model_validate({
502
+ "TariffBlocksLegend": [
503
+ {"BlockId": "EnergyCharge", "BlockRange": "0", "BlockPrice": "RM 0.2703"},
504
+ {"BlockId": "CustomerCharge", "BlockRange": "0", "BlockPrice": "RM 10.00"},
505
+ ],
506
+ })
507
+ assert len(group.items) == 2
508
+ assert group.items[0].block_id == "EnergyCharge"
509
+ assert group.items[1].block_price == "RM 10.00"
510
+
511
+ def test_empty(self):
512
+ group = TariffBlockLegendGroup.model_validate({"TariffBlocksLegend": []})
513
+ assert group.items == []
@@ -223,6 +223,39 @@ wheels = [
223
223
  { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" },
224
224
  ]
225
225
 
226
+ [[package]]
227
+ name = "curl-cffi"
228
+ version = "0.15.0"
229
+ source = { registry = "https://pypi.org/simple" }
230
+ dependencies = [
231
+ { name = "certifi" },
232
+ { name = "cffi" },
233
+ { name = "rich" },
234
+ ]
235
+ sdist = { url = "https://files.pythonhosted.org/packages/48/5b/89fcfebd3e5e85134147ac99e9f2b2271165fd4d71984fc65da5f17819b7/curl_cffi-0.15.0.tar.gz", hash = "sha256:ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded", size = 196437, upload-time = "2026-04-03T11:12:31.525Z" }
236
+ wheels = [
237
+ { url = "https://files.pythonhosted.org/packages/5e/42/54ddd442c795f30ce5dd4e49f87ce77505958d3777cd96a91567a3975d2a/curl_cffi-0.15.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bda66404010e9ed743b1b83c20c86f24fe21a9a6873e17479d6e67e29d8ded28", size = 2795267, upload-time = "2026-04-03T11:11:46.48Z" },
238
+ { url = "https://files.pythonhosted.org/packages/83/2d/3915e238579b3c5a92cead5c79130c3b8d20caaba7616cc4d894650e1d6b/curl_cffi-0.15.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a25620d9bf989c9c029a7d1642999c4c265abb0bad811deb2f77b0b5b2b12e5b", size = 2573544, upload-time = "2026-04-03T11:11:47.951Z" },
239
+ { url = "https://files.pythonhosted.org/packages/2a/b3/9d2f1057749a1b07ba1989db3c1503ce8bed998310bae9aea2c43aa64f20/curl_cffi-0.15.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:582e570aa2586b96ed47cf4a17586b9a3c462cbe43f780487c3dc245c6ef1527", size = 10515369, upload-time = "2026-04-03T11:11:50.126Z" },
240
+ { url = "https://files.pythonhosted.org/packages/b5/1d/6d10dded5ce3fd8157e558ebd97d09e551b77a62cdc1c31e93d0a633cee5/curl_cffi-0.15.0-cp310-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:838e48212447d9c81364b04707a5c861daf08f8320f9ecb3406a8919d1d5c3b3", size = 10160045, upload-time = "2026-04-03T11:11:52.664Z" },
241
+ { url = "https://files.pythonhosted.org/packages/5c/12/c70b835487ace3b9ba1502631912e3440082b8ae3a162f60b59cb0b6444d/curl_cffi-0.15.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b6c847d86283b07ae69bb72c82eb8a59242277142aa35b89850f89e792a02fc", size = 11090433, upload-time = "2026-04-03T11:11:55.049Z" },
242
+ { url = "https://files.pythonhosted.org/packages/ea/0d/78edcc4f71934225db99df68197a107386d59080742fc7bf6bb4d007924f/curl_cffi-0.15.0-cp310-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e5e69eee735f659287e2c84444319d68a1fa68dd37abf228943a4074864283a", size = 10479178, upload-time = "2026-04-03T11:11:57.685Z" },
243
+ { url = "https://files.pythonhosted.org/packages/5b/84/1e101c1acb1ea2f0b4992f5c3024f596d8e21db0d53540b9d583f673c4e7/curl_cffi-0.15.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1323950224db24f4c510d010b3affa02196ca853fb424191fa917a513d3f4b", size = 10317051, upload-time = "2026-04-03T11:12:00.295Z" },
244
+ { url = "https://files.pythonhosted.org/packages/28/42/8ef236b22a6c23d096c85a1dc507efe37bfdfc7a2f8a4b34efb590197369/curl_cffi-0.15.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:41f80170ba844009273b2660da1964ec31e99e5719d16b3422ada87177e32e13", size = 11299660, upload-time = "2026-04-03T11:12:02.791Z" },
245
+ { url = "https://files.pythonhosted.org/packages/1d/01/56aeb055d962da87a1be0d74c6c644e251c7e88129b5471dc44ac724e678/curl_cffi-0.15.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1977e1e12cfb5c11352cbb74acef1bed24eb7d226dab61ca57c168c21acd4d61", size = 11945049, upload-time = "2026-04-03T11:12:05.912Z" },
246
+ { url = "https://files.pythonhosted.org/packages/d8/8c/2abf99a38d6340d66cf0557e0c750ef3f8883dfc5d450087e01c85861343/curl_cffi-0.15.0-cp310-abi3-win_amd64.whl", hash = "sha256:5a0c1896a0d5a5ac1eb89cd24b008d2b718dd1df6fd2f75451b59ca66e49e572", size = 1661649, upload-time = "2026-04-03T11:12:07.948Z" },
247
+ { url = "https://files.pythonhosted.org/packages/3d/39/dfd54f2240d3a9b96d77bacc62b97813b35e2aa8ecf5cd5013c683f1ba96/curl_cffi-0.15.0-cp310-abi3-win_arm64.whl", hash = "sha256:a6d57f8389273a3a1f94370473c74897467bcc36af0a17336989780c507fa43d", size = 1410741, upload-time = "2026-04-03T11:12:10.073Z" },
248
+ { url = "https://files.pythonhosted.org/packages/19/6a/c24df8a4fc22fa84070dcd94abeba43c15e08cc09e35869565c0bad196fd/curl_cffi-0.15.0-cp313-abi3-android_24_arm64_v8a.whl", hash = "sha256:4682dc38d4336e0eb0b185374db90a760efde63cbea994b4e63f3521d44c4c92", size = 7190427, upload-time = "2026-04-03T11:12:12.142Z" },
249
+ { url = "https://files.pythonhosted.org/packages/11/56/132225cb3491d07cc6adcce5fe395e059bde87c68cff1ef87a31c88c7819/curl_cffi-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:967ad7355bd8e9586f8c2d02eaa99953747549e7ea4a9b25cd53353e6b67fe6d", size = 2795723, upload-time = "2026-04-03T11:12:13.668Z" },
250
+ { url = "https://files.pythonhosted.org/packages/07/8f/f4f83cd303bef7e8f1749512e5dd157e7e5d08b0a36c8211f9640a2757bf/curl_cffi-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e63539d0d839d0a8c5eacf86229bc68c57803547f35e0db7ee0986328b478c3", size = 2573739, upload-time = "2026-04-03T11:12:15.08Z" },
251
+ { url = "https://files.pythonhosted.org/packages/e8/5c/643d65c7fc9acd742876aa55c2d7823c438cb7665810acd2e66c9976c4d9/curl_cffi-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08c799b89740b9bc49c09fbc3d5907f13ac1f845ca52620507ef9466d4639dd5", size = 10521046, upload-time = "2026-04-03T11:12:17.034Z" },
252
+ { url = "https://files.pythonhosted.org/packages/7f/0b/9b8037113c93f4c5323096163471fa7c35c7676c3f608eeaf1287cd99d58/curl_cffi-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b7a92767a888ee90147e18964b396d8435ff42737030d6fb00824ffd6094805", size = 11096115, upload-time = "2026-04-03T11:12:19.694Z" },
253
+ { url = "https://files.pythonhosted.org/packages/5f/96/fff2fcbd924ef4042e0d67379f751a8a4e3186a91e75e35a4cf218b306ee/curl_cffi-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:829cc357061ecb99cc2d406301f609a039e05665322f5c025ec67c38b0dc49ce", size = 11305346, upload-time = "2026-04-03T11:12:22.151Z" },
254
+ { url = "https://files.pythonhosted.org/packages/53/1b/304b253a45ab28691c8c5e8cca1e6cbb9cf8e46dfceae4648dd536f75e73/curl_cffi-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:408d6f14e346841cd889c2e0962832bb235ba3b6749ebf609f347f747da5e60f", size = 11949834, upload-time = "2026-04-03T11:12:24.986Z" },
255
+ { url = "https://files.pythonhosted.org/packages/5a/ff/4723d92f08259c707a974aba27a08d0a822b9555e35ca581bf18d055a364/curl_cffi-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b624c7ce087bfda967a013ed0a64702a525444e5b6e97d23534d567ccc6525aa", size = 1702771, upload-time = "2026-04-03T11:12:28.201Z" },
256
+ { url = "https://files.pythonhosted.org/packages/59/8c/36bbe06d66fa2b765e4a07199f643a59a9cd1a754207a96335402a9520f4/curl_cffi-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0b6c0543b993996670e9e4b78e305a2d60809d5681903ffb5568e21a387434d3", size = 1466312, upload-time = "2026-04-03T11:12:30.054Z" },
257
+ ]
258
+
226
259
  [[package]]
227
260
  name = "dill"
228
261
  version = "0.4.1"
@@ -567,15 +600,15 @@ wheels = [
567
600
 
568
601
  [[package]]
569
602
  name = "python-mytnb"
570
- version = "0.2.0"
603
+ version = "0.3.0"
571
604
  source = { editable = "." }
572
605
  dependencies = [
573
606
  { name = "click" },
574
607
  { name = "cryptography" },
608
+ { name = "curl-cffi" },
575
609
  { name = "httpx" },
576
610
  { name = "pydantic" },
577
611
  { name = "rich" },
578
- { name = "tls-client" },
579
612
  ]
580
613
 
581
614
  [package.optional-dependencies]
@@ -590,6 +623,7 @@ dev = [
590
623
  requires-dist = [
591
624
  { name = "click", specifier = ">=8.3.3" },
592
625
  { name = "cryptography", specifier = ">=42.0" },
626
+ { name = "curl-cffi", specifier = ">=0.7" },
593
627
  { name = "httpx", specifier = ">=0.27" },
594
628
  { name = "pydantic", specifier = ">=2.0" },
595
629
  { name = "pylint", marker = "extra == 'dev'", specifier = ">=3.0" },
@@ -597,7 +631,6 @@ requires-dist = [
597
631
  { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21" },
598
632
  { name = "rich", specifier = ">=15.0.0" },
599
633
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" },
600
- { name = "tls-client", specifier = ">=1.0" },
601
634
  ]
602
635
  provides-extras = ["dev"]
603
636
 
@@ -639,15 +672,6 @@ wheels = [
639
672
  { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
640
673
  ]
641
674
 
642
- [[package]]
643
- name = "tls-client"
644
- version = "1.0.1"
645
- source = { registry = "https://pypi.org/simple" }
646
- sdist = { url = "https://files.pythonhosted.org/packages/3c/a6/6ec27c66a836a11a085e841f825d2f5bd289092f3bcd2f645558f587c89f/tls_client-1.0.1.tar.gz", hash = "sha256:dad797f3412bb713606e0765d489f547ffb580c5ffdb74aed47a183ce8505ff5", size = 16414, upload-time = "2024-02-02T18:55:55.767Z" }
647
- wheels = [
648
- { url = "https://files.pythonhosted.org/packages/75/cd/5c735818692927e07980357445569adb6ee204c3332d19c516bae01c6cfa/tls_client-1.0.1-py3-none-any.whl", hash = "sha256:2f8915c0642c2226c9e33120072a2af082812f6310d32f4ea4da322db7d3bb1c", size = 41287556, upload-time = "2024-02-02T18:55:52.226Z" },
649
- ]
650
-
651
675
  [[package]]
652
676
  name = "tomli"
653
677
  version = "2.4.1"
File without changes
File without changes