moy-nalog-api 1.0.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.
@@ -0,0 +1,25 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(python -m pytest:*)",
5
+ "Bash(source .venv/bin/activate)",
6
+ "Bash(pip install:*)",
7
+ "Bash(pytest:*)",
8
+ "Bash(.venv/bin/pip install:*)",
9
+ "Bash(uv run pytest:*)",
10
+ "Bash(uv run ruff check:*)",
11
+ "Bash(uv pip install:*)",
12
+ "Bash(uv run python:*)",
13
+ "Bash(git init:*)",
14
+ "Bash(git add:*)",
15
+ "Bash(git commit:*)",
16
+ "Bash(git branch:*)",
17
+ "Bash(git remote add:*)",
18
+ "Bash(git push:*)",
19
+ "Bash(git remote set-url:*)",
20
+ "Bash(gh auth:*)",
21
+ "Bash(cat:*)",
22
+ "Bash(python -m build:*)"
23
+ ]
24
+ }
25
+ }
@@ -0,0 +1,43 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ *.egg
11
+ .eggs/
12
+
13
+ # Virtual environments
14
+ venv/
15
+ .venv/
16
+ env/
17
+
18
+ # Environment
19
+ .env
20
+ .env.local
21
+ session.json
22
+ *.pkl
23
+
24
+ # IDE
25
+ .idea/
26
+ .vscode/
27
+ *.swp
28
+ *.swo
29
+ .DS_Store
30
+
31
+ # Testing
32
+ .pytest_cache/
33
+ .coverage
34
+ htmlcov/
35
+ .tox/
36
+ .mypy_cache/
37
+ .ruff_cache/
38
+ test_output/
39
+
40
+ # Misc
41
+ *.log
42
+ Thumbs.db
43
+ uv.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kirill Nikulin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,533 @@
1
+ Metadata-Version: 2.4
2
+ Name: moy-nalog-api
3
+ Version: 1.0.0
4
+ Summary: The most complete async Python client for Russian self-employed tax service (Moy Nalog / lknpd.nalog.ru)
5
+ Project-URL: Homepage, https://github.com/inache-su/moy-nalog-api
6
+ Project-URL: Documentation, https://github.com/inache-su/moy-nalog-api#readme
7
+ Project-URL: Repository, https://github.com/inache-su/moy-nalog-api
8
+ Project-URL: Issues, https://github.com/inache-su/moy-nalog-api/issues
9
+ Project-URL: Changelog, https://github.com/inache-su/moy-nalog-api/blob/main/CHANGELOG.md
10
+ Author-email: Kirill Nikulin <me@kirodev.eu>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,async,fns,httpx,moy-nalog,nalog,npd,pydantic,receipt,russia,samozanyatiy,self-employed,tax
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Framework :: Pydantic :: 2
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: httpx>=0.25.0
29
+ Requires-Dist: pydantic>=2.0.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
32
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
33
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
34
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
35
+ Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
36
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # moy-nalog-api
40
+
41
+ [![GitHub](https://img.shields.io/badge/GitHub-inache--su%2Fmoy--nalog--api-181717?logo=github)](https://github.com/inache-su/moy-nalog-api)
42
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
43
+ [![PyPI version](https://badge.fury.io/py/moy-nalog-api.svg)](https://badge.fury.io/py/moy-nalog-api)
44
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
45
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
46
+
47
+ **The most complete and modern Python client for Russian self-employed tax service (lknpd.nalog.ru).**
48
+
49
+ Unofficial Python client for "Moy Nalog" API (self-employed, NPD tax regime).
50
+
51
+ [Документация на русском](README.ru.md)
52
+
53
+ ## Why moy-nalog-api?
54
+
55
+ There are several Python libraries for the Moy Nalog API. Here's why you should choose this one:
56
+
57
+ | Feature | moy-nalog-api | Others |
58
+ |---------|---------------|--------|
59
+ | **Async/await support** | Native httpx async | Often sync-only or requests-based |
60
+ | **Sync wrapper included** | Yes, for non-async code | Usually one or the other |
61
+ | **SMS authentication** | Full support (request + verify) | Often missing or broken |
62
+ | **Session persistence** | Built-in JSON file storage | Manual implementation required |
63
+ | **Auto token refresh** | Automatic before expiration | Manual refresh needed |
64
+ | **Type hints** | 100% typed, mypy-compatible | Partial or none |
65
+ | **Pydantic v2** | Full validation and serialization | Often dict-based or Pydantic v1 |
66
+ | **Modern Python** | 3.10+ with latest syntax | Often 3.7+ with legacy code |
67
+ | **Error handling** | Typed exception hierarchy | Generic exceptions |
68
+ | **Retry logic** | Exponential backoff built-in | Usually none |
69
+ | **Multiple items** | Native support for multi-item receipts | Single item only |
70
+ | **Documentation** | Comprehensive with examples | Often minimal |
71
+
72
+ ## Features
73
+
74
+ - Async (httpx) and sync client support
75
+ - Password and SMS authentication
76
+ - Automatic token refresh with session persistence
77
+ - Retry with exponential backoff
78
+ - Full Pydantic v2 validation
79
+ - Multiple receipt items in single receipt
80
+ - All client types (individual, legal entity, foreign)
81
+ - Income list with pagination and filtering
82
+ - Receipt cancellation with reason
83
+ - Complete type hints for IDE support
84
+
85
+ ## Installation
86
+
87
+ ```bash
88
+ pip install moy-nalog-api
89
+ ```
90
+
91
+ For development:
92
+ ```bash
93
+ pip install moy-nalog-api[dev]
94
+ ```
95
+
96
+ ## Quick Start
97
+
98
+ ### Async (Recommended)
99
+
100
+ ```python
101
+ import asyncio
102
+ from decimal import Decimal
103
+ from moy_nalog import MoyNalogClient
104
+
105
+ async def main():
106
+ # Create client with session persistence
107
+ async with MoyNalogClient(session_file="session.json") as client:
108
+
109
+ # First run: authenticate
110
+ if not client.is_authenticated:
111
+ await client.auth_by_password("your_inn", "your_password")
112
+
113
+ # Create receipt
114
+ receipt = await client.create_receipt(
115
+ name="Consulting services",
116
+ amount=Decimal("5000.00")
117
+ )
118
+
119
+ print(f"Receipt created: {receipt.print_url}")
120
+
121
+ asyncio.run(main())
122
+ ```
123
+
124
+ ### Sync
125
+
126
+ ```python
127
+ from decimal import Decimal
128
+ from moy_nalog import MoyNalogClientSync
129
+
130
+ with MoyNalogClientSync(session_file="session.json") as client:
131
+ if not client.is_authenticated:
132
+ client.auth_by_password("your_inn", "your_password")
133
+
134
+ receipt = client.create_receipt(
135
+ name="Consulting services",
136
+ amount=Decimal("5000.00")
137
+ )
138
+
139
+ print(f"Receipt created: {receipt.print_url}")
140
+ ```
141
+
142
+ ## Authentication
143
+
144
+ ### Password Authentication
145
+
146
+ Use your INN (tax identification number) or phone and password from nalog.ru:
147
+
148
+ ```python
149
+ profile = await client.auth_by_password(
150
+ username="123456789012", # INN (12 digits) or phone
151
+ password="your_password"
152
+ )
153
+ print(f"Authenticated as: {profile.display_name}")
154
+ print(f"INN: {profile.inn}")
155
+ print(f"Status: {profile.status}")
156
+ ```
157
+
158
+ ### SMS Authentication
159
+
160
+ Two-step process for phone-based authentication:
161
+
162
+ ```python
163
+ # Step 1: Request SMS code
164
+ phone = "79001234567" # Format: 7XXXXXXXXXX (11 digits)
165
+ challenge = await client.request_sms_code(phone)
166
+ print(f"SMS sent! Code expires in {challenge.expire_in} seconds")
167
+
168
+ # Step 2: Enter code and authenticate
169
+ code = input("Enter 6-digit code from SMS: ")
170
+ profile = await client.auth_by_sms(phone, challenge.challenge_token, code)
171
+ print(f"Authenticated as: {profile.display_name}")
172
+ ```
173
+
174
+ ### Session Persistence
175
+
176
+ Save and restore authentication tokens automatically:
177
+
178
+ ```python
179
+ # Session file stores tokens between runs
180
+ client = MoyNalogClient(session_file="session.json")
181
+
182
+ # Check if already authenticated from previous session
183
+ if client.is_authenticated:
184
+ print("Session restored from file")
185
+ else:
186
+ # Authenticate (tokens saved automatically)
187
+ await client.auth_by_password(username, password)
188
+
189
+ # Tokens auto-refresh when expired
190
+ # Session auto-saves on close
191
+ ```
192
+
193
+ Session file contains:
194
+ - Access token (for API requests)
195
+ - Refresh token (for token renewal)
196
+ - Token expiration time
197
+ - User INN and device ID
198
+
199
+ ## Creating Receipts
200
+
201
+ ### Simple Receipt
202
+
203
+ ```python
204
+ from decimal import Decimal
205
+
206
+ receipt = await client.create_receipt(
207
+ name="Web development",
208
+ amount=Decimal("15000.00")
209
+ )
210
+
211
+ print(f"UUID: {receipt.uuid}")
212
+ print(f"Amount: {receipt.total_amount} RUB")
213
+ print(f"Print URL: {receipt.print_url}")
214
+ print(f"JSON URL: {receipt.json_url}")
215
+ ```
216
+
217
+ ### Multiple Items
218
+
219
+ ```python
220
+ from decimal import Decimal
221
+ from moy_nalog import ServiceItem
222
+
223
+ items = [
224
+ ServiceItem(name="Consulting", amount=Decimal("3000"), quantity=2),
225
+ ServiceItem(name="Development", amount=Decimal("10000"), quantity=1),
226
+ ServiceItem(name="Support", amount=Decimal("500"), quantity=4),
227
+ ]
228
+
229
+ receipt = await client.create_receipt_multi(items)
230
+ # Total: 3000*2 + 10000*1 + 500*4 = 18000 RUB
231
+ print(f"Total: {receipt.total_amount} RUB")
232
+ ```
233
+
234
+ ### With Client Information
235
+
236
+ #### Individual Client (default)
237
+
238
+ ```python
239
+ from moy_nalog import Client, IncomeType
240
+
241
+ client_info = Client(
242
+ income_type=IncomeType.INDIVIDUAL,
243
+ display_name="Ivan Petrov",
244
+ contact_phone="+79001234567"
245
+ )
246
+
247
+ receipt = await client.create_receipt(
248
+ name="Service",
249
+ amount=Decimal("1000"),
250
+ client=client_info
251
+ )
252
+ ```
253
+
254
+ #### Legal Entity (Company)
255
+
256
+ ```python
257
+ company = Client(
258
+ income_type=IncomeType.LEGAL_ENTITY,
259
+ display_name="OOO Romashka",
260
+ inn="7712345678" # 10 digits for companies
261
+ )
262
+
263
+ receipt = await client.create_receipt(
264
+ name="B2B Service",
265
+ amount=Decimal("50000"),
266
+ client=company
267
+ )
268
+ ```
269
+
270
+ #### Foreign Organization
271
+
272
+ ```python
273
+ foreign = Client(
274
+ income_type=IncomeType.FOREIGN_AGENCY,
275
+ display_name="Acme Corporation",
276
+ inn="9909123456"
277
+ )
278
+
279
+ receipt = await client.create_receipt(
280
+ name="International consulting",
281
+ amount=Decimal("100000"),
282
+ client=foreign
283
+ )
284
+ ```
285
+
286
+ ### Payment Types
287
+
288
+ ```python
289
+ from moy_nalog import PaymentType
290
+
291
+ # Cash or card payment (default)
292
+ receipt = await client.create_receipt(
293
+ name="Service",
294
+ amount=Decimal("1000"),
295
+ payment_type=PaymentType.CASH
296
+ )
297
+
298
+ # Bank transfer (requires legal entity client with INN)
299
+ receipt = await client.create_receipt(
300
+ name="Service",
301
+ amount=Decimal("50000"),
302
+ client=company, # Must have INN
303
+ payment_type=PaymentType.WIRE
304
+ )
305
+ ```
306
+
307
+ ## Canceling Receipts
308
+
309
+ Cancel a receipt within the same tax period:
310
+
311
+ ```python
312
+ from moy_nalog import CancelReason
313
+
314
+ # Client requested refund
315
+ await client.cancel_receipt(
316
+ receipt_uuid="abc123",
317
+ reason=CancelReason.REFUND
318
+ )
319
+
320
+ # Receipt created by mistake
321
+ await client.cancel_receipt(
322
+ receipt_uuid="abc123",
323
+ reason=CancelReason.MISTAKE
324
+ )
325
+ ```
326
+
327
+ ## Viewing Receipts
328
+
329
+ ### Get Income List
330
+
331
+ ```python
332
+ from datetime import datetime
333
+
334
+ # Get recent receipts (default: last 50)
335
+ incomes = await client.get_incomes()
336
+
337
+ for receipt in incomes.items:
338
+ status = "CANCELLED" if receipt.is_cancelled else "ACTIVE"
339
+ print(f"{receipt.uuid}: {receipt.total_amount} RUB [{status}]")
340
+
341
+ print(f"Total count: {incomes.total}")
342
+ print(f"Has more: {incomes.has_more}")
343
+ ```
344
+
345
+ ### With Filters and Pagination
346
+
347
+ ```python
348
+ incomes = await client.get_incomes(
349
+ from_date=datetime(2024, 1, 1),
350
+ to_date=datetime(2024, 12, 31),
351
+ offset=0,
352
+ limit=50
353
+ )
354
+
355
+ # Load more if needed
356
+ if incomes.has_more:
357
+ more = await client.get_incomes(offset=50, limit=50)
358
+ ```
359
+
360
+ ### Get Receipt Details
361
+
362
+ ```python
363
+ # Get full receipt data as dict
364
+ data = await client.get_receipt("receipt_uuid")
365
+ if data:
366
+ print(f"Services: {data['services']}")
367
+ print(f"Payment type: {data['paymentType']}")
368
+
369
+ # Get printable URL
370
+ url = client.get_receipt_print_url("receipt_uuid")
371
+ ```
372
+
373
+ ## Error Handling
374
+
375
+ ```python
376
+ from moy_nalog import (
377
+ MoyNalogError,
378
+ AuthenticationError,
379
+ InvalidCredentialsError,
380
+ TokenExpiredError,
381
+ SMSError,
382
+ SMSRateLimitError,
383
+ InvalidSMSCodeError,
384
+ ReceiptError,
385
+ ValidationError,
386
+ NetworkError,
387
+ RateLimitError,
388
+ )
389
+
390
+ try:
391
+ await client.auth_by_password(username, password)
392
+ except InvalidCredentialsError:
393
+ print("Wrong username or password")
394
+ except TokenExpiredError:
395
+ print("Session expired, re-authenticate")
396
+ except AuthenticationError as e:
397
+ print(f"Auth failed: {e.message}")
398
+
399
+ try:
400
+ await client.request_sms_code(phone)
401
+ except SMSRateLimitError:
402
+ print("Too many SMS requests, wait a minute")
403
+ except SMSError as e:
404
+ print(f"SMS error: {e.message}")
405
+
406
+ try:
407
+ await client.create_receipt("Service", Decimal("1000"))
408
+ except ReceiptError as e:
409
+ print(f"Receipt error: {e.message}")
410
+ print(f"Error code: {e.code}")
411
+ print(f"API response: {e.response}")
412
+
413
+ try:
414
+ # Network issues are retried automatically
415
+ await client.get_incomes()
416
+ except NetworkError:
417
+ print("Network unavailable after retries")
418
+ except RateLimitError:
419
+ print("API rate limit exceeded")
420
+ ```
421
+
422
+ ## Configuration
423
+
424
+ ```python
425
+ client = MoyNalogClient(
426
+ # Timezone for receipt timestamps (default: Europe/Moscow)
427
+ timezone="Europe/Moscow",
428
+
429
+ # Request timeout in seconds (default: 30)
430
+ timeout=30.0,
431
+
432
+ # Retry attempts for failed requests (default: 3)
433
+ max_retries=3,
434
+
435
+ # Path to session file for persistence (optional)
436
+ session_file="session.json",
437
+
438
+ # Auto-refresh tokens before expiration (default: True)
439
+ auto_refresh_token=True,
440
+ )
441
+ ```
442
+
443
+ ## User Profile
444
+
445
+ ```python
446
+ profile = await client.get_user_profile()
447
+
448
+ print(f"ID: {profile.id}")
449
+ print(f"INN: {profile.inn}")
450
+ print(f"Phone: {profile.phone}")
451
+ print(f"Email: {profile.email}")
452
+ print(f"Name: {profile.display_name}")
453
+ print(f"Full name: {profile.full_name}")
454
+ print(f"Status: {profile.status}")
455
+ print(f"Registration date: {profile.registration_date}")
456
+ ```
457
+
458
+ ## Testing
459
+
460
+ ### Unit Tests
461
+
462
+ ```bash
463
+ # Install dev dependencies
464
+ pip install -e ".[dev]"
465
+
466
+ # Run unit tests
467
+ pytest
468
+
469
+ # Run with coverage
470
+ pytest --cov=moy_nalog
471
+ ```
472
+
473
+ ### Integration Test
474
+
475
+ Interactive script for testing all API functionality with a real account.
476
+
477
+ ```bash
478
+ python scripts/integration_test.py
479
+ ```
480
+
481
+ **What it tests:**
482
+ - Password and SMS authentication
483
+ - Session persistence (save/restore tokens)
484
+ - Simple receipt creation (1 item, cash payment)
485
+ - Multi-item receipt (3 items with quantities)
486
+ - Receipt with individual client info
487
+ - Receipt with legal entity client (INN required)
488
+ - Receipt with bank transfer payment (WIRE)
489
+ - Income list retrieval with pagination
490
+ - Receipt data retrieval by UUID
491
+ - Receipt cancellation
492
+
493
+ **How it works:**
494
+ 1. Choose authentication method (password or SMS)
495
+ 2. Enter credentials
496
+ 3. Script runs all tests sequentially
497
+ 4. All created receipts are cancelled automatically
498
+ 5. Detailed log and JSON report are saved to `test_output/` directory
499
+
500
+ **Output:**
501
+ - `test_output/<timestamp>/test_log_*.log` - detailed execution log
502
+ - `test_output/<timestamp>/test_report_*.json` - JSON report with results
503
+ - `test_output/<timestamp>/receipts/` - downloaded receipt files (JSON/HTML)
504
+
505
+ ## Requirements
506
+
507
+ - Python 3.10+
508
+ - httpx >= 0.25.0
509
+ - pydantic >= 2.0.0
510
+
511
+ ## Disclaimer
512
+
513
+ This is an **unofficial** client. The API may change without notice. Use at your own risk. The author is not responsible for any issues with tax authorities.
514
+
515
+ Always verify receipts in your personal cabinet at [lknpd.nalog.ru](https://lknpd.nalog.ru).
516
+
517
+ ## Author
518
+
519
+ Kirill Nikulin (c) 2025 [kirodev.eu](https://kirodev.eu)
520
+
521
+ ## License
522
+
523
+ MIT License - see [LICENSE](LICENSE) file.
524
+
525
+ ## Contributing
526
+
527
+ Contributions are welcome! Please:
528
+ 1. Fork the repository
529
+ 2. Create a feature branch
530
+ 3. Make your changes
531
+ 4. Run tests: `pytest`
532
+ 5. Run linting: `ruff check .`
533
+ 6. Submit a pull request