gst-validator 0.1.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.
- gst_validator-0.1.0/LICENSE +21 -0
- gst_validator-0.1.0/PKG-INFO +512 -0
- gst_validator-0.1.0/README.md +486 -0
- gst_validator-0.1.0/pyproject.toml +84 -0
- gst_validator-0.1.0/pyproject.toml.orig +64 -0
- gst_validator-0.1.0/src/gst_validator/__init__.py +46 -0
- gst_validator-0.1.0/src/gst_validator/__main__.py +3 -0
- gst_validator-0.1.0/src/gst_validator/cache.py +94 -0
- gst_validator-0.1.0/src/gst_validator/cli.py +157 -0
- gst_validator-0.1.0/src/gst_validator/client.py +413 -0
- gst_validator-0.1.0/src/gst_validator/exceptions.py +37 -0
- gst_validator-0.1.0/src/gst_validator/models.py +596 -0
- gst_validator-0.1.0/src/gst_validator/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rahul Gurujala
|
|
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,512 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gst-validator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Validate GSTINs and fetch taxpayer details from the Indian GST portal
|
|
5
|
+
Keywords: gst,gstin,india,tax,validator,taxpayer
|
|
6
|
+
Author: rahulgurujala
|
|
7
|
+
Author-email: rahulgurujala <isaacnewtonrahul@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: httpx>=0.28.1
|
|
21
|
+
Requires-Python: >=3.13
|
|
22
|
+
Project-URL: Homepage, https://github.com/rahulgurujala/gst-validator
|
|
23
|
+
Project-URL: Repository, https://github.com/rahulgurujala/gst-validator
|
|
24
|
+
Project-URL: Issues, https://github.com/rahulgurujala/gst-validator/issues
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# gst-validator
|
|
28
|
+
|
|
29
|
+
Validate Indian GSTINs offline and pull taxpayer details from the public GST
|
|
30
|
+
portal. Ships as a typed library (`import gst_validator`) and a CLI
|
|
31
|
+
(`gst-validator`). The CLI is a thin wrapper over the same public API, so
|
|
32
|
+
anything it does, your app can do.
|
|
33
|
+
|
|
34
|
+
- Offline GSTIN validation: format **and** mod-36 checksum, no network
|
|
35
|
+
- Structured objects, not raw dicts: dates parsed, `"NA"`/`""` normalised to `None`
|
|
36
|
+
- Captcha as bytes / base64 / data URI, so a browser or a human can solve it
|
|
37
|
+
- Sync and async clients, strict-typed, `py.typed`
|
|
38
|
+
- Built-in TTL cache, because each lookup costs one human-solved captcha
|
|
39
|
+
- Three extra portal endpoints that need **no captcha** at all
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv add gst-validator # into your project
|
|
45
|
+
uv sync # working on this repo
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires Python 3.13+. Only runtime dependency: `httpx`.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
# CLI
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
gst-validator [-h] [--offline] [--json] [--details-only] [--raw]
|
|
56
|
+
[--captcha-path PATH] [--captcha-base64] [--refresh]
|
|
57
|
+
[--keep-captcha] GSTIN
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Validate without touching the network
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
$ gst-validator 27AAACR5055K1Z7 --offline
|
|
64
|
+
27AAACR5055K1Z7 is valid (state 27, PAN AAACR5055K)
|
|
65
|
+
|
|
66
|
+
$ gst-validator 27AAACR5055K1Z7 --offline --json
|
|
67
|
+
{"gstin": "27AAACR5055K1Z7", "state_code": "27", "pan": "AAACR5055K"}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use this in CI, in a pre-commit check, or to screen input before spending a
|
|
71
|
+
captcha. Exit code `2` means the GSTIN is malformed.
|
|
72
|
+
|
|
73
|
+
### Full lookup (interactive)
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
$ gst-validator 27AAACR5055K1Z7
|
|
77
|
+
captcha image written to /tmp/27AAACR5055K1Z7-captcha.png # stderr
|
|
78
|
+
captcha text: 784077
|
|
79
|
+
gstin 27AAACR5055K1Z7
|
|
80
|
+
legal_name <registered name as the portal returns it>
|
|
81
|
+
status Active
|
|
82
|
+
principal_address <registered place of business, one line>
|
|
83
|
+
goods_and_services 39269080 - POLYPROPYLENE ARTICLES, NOT ELSEWHERE SPECIFIED...
|
|
84
|
+
financial_years 2017-2018, 2018-2019, ...
|
|
85
|
+
...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Open the image, type the text. The file is deleted once you have entered it.
|
|
89
|
+
|
|
90
|
+
### Machine-readable output
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
gst-validator 27AAACR5055K1Z7 --json # modelled fields, dates as ISO strings
|
|
94
|
+
gst-validator 27AAACR5055K1Z7 --raw # the portal's body verbatim, nothing dropped
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`--json` is the one to parse: stable key names, `null` instead of `"NA"`,
|
|
98
|
+
dates as `2025-09-15`. `--raw` is for debugging what the portal actually sent.
|
|
99
|
+
Both go to stdout; progress messages go to stderr, so piping is safe:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
gst-validator 27AAACR5055K1Z7 --json | jq -r '.legal_name, .principal_address'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Solving the captcha somewhere else
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
$ gst-validator 27AAACR5055K1Z7 --captcha-base64
|
|
109
|
+
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALY...
|
|
110
|
+
captcha text: 784077
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The data URI goes to stdout. Paste it into a browser address bar, drop it in
|
|
114
|
+
an `<img src=...>`, or hand it to a solving service. The process keeps the
|
|
115
|
+
portal session open while it waits on stdin, which is what makes this work.
|
|
116
|
+
|
|
117
|
+
### Other flags
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
gst-validator 27AAACR5055K1Z7 --details-only # skip the captcha-free extras
|
|
121
|
+
gst-validator 27AAACR5055K1Z7 --refresh # ignore the cache, force a fresh lookup
|
|
122
|
+
gst-validator 27AAACR5055K1Z7 --keep-captcha # keep the image file for inspection
|
|
123
|
+
gst-validator 27AAACR5055K1Z7 --captcha-path ./c.png # write it where you want
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Also runnable as a module: `python -m gst_validator 27AAACR5055K1Z7`.
|
|
127
|
+
|
|
128
|
+
### Exit codes
|
|
129
|
+
|
|
130
|
+
| Code | Meaning |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `0` | success |
|
|
133
|
+
| `1` | lookup failed (wrong captcha, portal error, network) |
|
|
134
|
+
| `2` | GSTIN failed format or checksum validation |
|
|
135
|
+
| `130` | aborted (Ctrl-C / EOF) |
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
if gst-validator "$GSTIN" --offline >/dev/null 2>&1; then
|
|
139
|
+
echo "well-formed"
|
|
140
|
+
fi
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
# Using it in your app
|
|
146
|
+
|
|
147
|
+
Everything is importable from the package root:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from gst_validator import (
|
|
151
|
+
GSTIN,
|
|
152
|
+
Captcha,
|
|
153
|
+
GSTClient,
|
|
154
|
+
AsyncGSTClient,
|
|
155
|
+
TaxpayerDetails,
|
|
156
|
+
TaxpayerProfile,
|
|
157
|
+
Address,
|
|
158
|
+
Jurisdiction,
|
|
159
|
+
GoodsOrService,
|
|
160
|
+
FinancialYear,
|
|
161
|
+
FilingPreference,
|
|
162
|
+
TTLCache,
|
|
163
|
+
NullCache,
|
|
164
|
+
DEFAULT_CACHE,
|
|
165
|
+
TaxpayerCache,
|
|
166
|
+
GSTValidatorError,
|
|
167
|
+
InvalidGSTINError,
|
|
168
|
+
CaptchaError,
|
|
169
|
+
TaxpayerLookupError,
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## 1. Validate a GSTIN (no network, no captcha)
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from gst_validator import GSTIN, InvalidGSTINError
|
|
177
|
+
|
|
178
|
+
GSTIN.is_valid("27AAACR5055K1Z7") # True - never raises
|
|
179
|
+
GSTIN.is_valid("27AAACR5055K1ZA") # False - checksum digit is wrong
|
|
180
|
+
|
|
181
|
+
gstin = GSTIN.parse(" 27aatcm7522p1zj ") # strips, upper-cases, validates
|
|
182
|
+
gstin.value # '27AAACR5055K1Z7'
|
|
183
|
+
gstin.state_code # '27'
|
|
184
|
+
gstin.state_name # 'Maharashtra'
|
|
185
|
+
gstin.pan # 'AAACR5055K'
|
|
186
|
+
gstin.entity_type # 'Company' (4th PAN character)
|
|
187
|
+
gstin.registration_sequence # '1' (Nth registration of this PAN in this state)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
GSTIN.parse(user_input)
|
|
191
|
+
except InvalidGSTINError as error:
|
|
192
|
+
print(error.value, error.reason) # the input, and why it was rejected
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`GSTIN` is a frozen dataclass: hashable, comparable, usable as a dict key.
|
|
196
|
+
Take one as a function parameter and malformed input cannot reach your code.
|
|
197
|
+
|
|
198
|
+
## 2. The data you get without a captcha
|
|
199
|
+
|
|
200
|
+
Three portal endpoints return data with no captcha at all, verified against
|
|
201
|
+
the live portal with no cookies and no prior captcha solve. The client still
|
|
202
|
+
opens a session first, since the portal could tighten this at any time:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from gst_validator import GSTClient
|
|
206
|
+
|
|
207
|
+
with GSTClient() as client:
|
|
208
|
+
client.fetch_goods_and_services("27AAACR5055K1Z7")
|
|
209
|
+
# (GoodsOrService(code='55151190', description='OTHER', is_service=False),
|
|
210
|
+
# GoodsOrService(code='39269080', description='POLYPROPYLENE ARTICLES, NOT
|
|
211
|
+
# ELSEWHERE SPECIFIED OR INCLUDED', is_service=False), ...)
|
|
212
|
+
# Service providers come back as SAC codes instead, with is_service=True:
|
|
213
|
+
# GoodsOrService(code='998314', description='Information technology
|
|
214
|
+
# design and development services', is_service=True)
|
|
215
|
+
|
|
216
|
+
client.fetch_financial_years("27AAACR5055K1Z7")
|
|
217
|
+
# (FinancialYear(label='2025-2026', value='2025'), FinancialYear('2026-2027', '2026'))
|
|
218
|
+
|
|
219
|
+
client.fetch_filing_preferences("27AAACR5055K1Z7")
|
|
220
|
+
# (FilingPreference(quarter='Q1', preference='Q'), ...) -> .is_quarterly / .is_monthly
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## 3. The full lookup (one captcha)
|
|
224
|
+
|
|
225
|
+
The captcha is bound to the client's cookies, so fetch and submit must happen
|
|
226
|
+
on the **same instance**:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
with GSTClient() as client:
|
|
230
|
+
captcha = client.fetch_captcha()
|
|
231
|
+
solved = input(f"solve this: {captcha.data_uri}\n> ")
|
|
232
|
+
profile = client.fetch_profile("27AAACR5055K1Z7", solved)
|
|
233
|
+
|
|
234
|
+
profile.name # registered trade name, else the legal name
|
|
235
|
+
profile.is_active # True
|
|
236
|
+
profile.details # TaxpayerDetails
|
|
237
|
+
profile.as_dict() # everything, JSON-ready
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`fetch_details()` instead of `fetch_profile()` if you only want the
|
|
241
|
+
captcha-gated part.
|
|
242
|
+
|
|
243
|
+
## 4. Web app: captcha to the browser, text back
|
|
244
|
+
|
|
245
|
+
The pattern the original Flask app was reaching for: keep one client per
|
|
246
|
+
pending lookup, keyed by a session id:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
import uuid
|
|
250
|
+
from fastapi import FastAPI, HTTPException
|
|
251
|
+
from gst_validator import GSTClient, GSTValidatorError, InvalidGSTINError
|
|
252
|
+
|
|
253
|
+
app = FastAPI()
|
|
254
|
+
pending: dict[str, GSTClient] = {} # swap for Redis + a TTL in production
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@app.post("/captcha")
|
|
258
|
+
def start() -> dict[str, str]:
|
|
259
|
+
client = GSTClient()
|
|
260
|
+
captcha = client.fetch_captcha()
|
|
261
|
+
session_id = str(uuid.uuid4())
|
|
262
|
+
pending[session_id] = client
|
|
263
|
+
return {"session_id": session_id, "image": captcha.data_uri}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@app.post("/lookup")
|
|
267
|
+
def lookup(session_id: str, gstin: str, captcha: str) -> dict[str, object]:
|
|
268
|
+
client = pending.pop(session_id, None)
|
|
269
|
+
if client is None:
|
|
270
|
+
raise HTTPException(400, "unknown or expired session")
|
|
271
|
+
try:
|
|
272
|
+
return client.fetch_profile(gstin, captcha).as_dict()
|
|
273
|
+
except InvalidGSTINError as error:
|
|
274
|
+
raise HTTPException(422, str(error)) from error
|
|
275
|
+
except GSTValidatorError as error:
|
|
276
|
+
raise HTTPException(502, str(error)) from error
|
|
277
|
+
finally:
|
|
278
|
+
client.close()
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The front end renders `image` straight into `<img src="{{ image }}">`, since it is
|
|
282
|
+
already a `data:` URI. Give `pending` an expiry; portal sessions do not live
|
|
283
|
+
forever, and an abandoned entry leaks a connection pool.
|
|
284
|
+
|
|
285
|
+
## 5. Async
|
|
286
|
+
|
|
287
|
+
Same API, `await` and `async with`:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
import asyncio
|
|
291
|
+
from gst_validator import AsyncGSTClient
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
async def codes(gstin: str) -> tuple[str, ...]:
|
|
295
|
+
async with AsyncGSTClient() as client:
|
|
296
|
+
items = await client.fetch_goods_and_services(gstin)
|
|
297
|
+
return tuple(item.code or "" for item in items)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
asyncio.run(codes("27AAACR5055K1Z7"))
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## 6. Caching
|
|
304
|
+
|
|
305
|
+
Each live lookup costs a human-solved captcha, so successful results are
|
|
306
|
+
cached in a process-wide `TTLCache` (24 h, 512 entries, LRU, thread-safe).
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
from gst_validator import DEFAULT_CACHE, GSTClient, NullCache, TTLCache
|
|
310
|
+
|
|
311
|
+
GSTClient() # shares DEFAULT_CACHE
|
|
312
|
+
GSTClient(cache=TTLCache(ttl=300)) # private, 5-minute cache
|
|
313
|
+
GSTClient(cache=NullCache()) # caching off
|
|
314
|
+
|
|
315
|
+
with GSTClient() as client:
|
|
316
|
+
if (hit := client.cached(gstin)) is not None:
|
|
317
|
+
details = hit # no captcha spent
|
|
318
|
+
else:
|
|
319
|
+
details = client.fetch_details(gstin, solved)
|
|
320
|
+
|
|
321
|
+
client.fetch_details(gstin, solved, refresh=True) # bypass and overwrite
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Back it with anything that satisfies the `TaxpayerCache` protocol:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
import json
|
|
328
|
+
from gst_validator import TaxpayerDetails
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class RedisCache:
|
|
332
|
+
def __init__(self, redis, ttl: int = 86_400) -> None:
|
|
333
|
+
self._redis, self._ttl = redis, ttl
|
|
334
|
+
|
|
335
|
+
def get(self, gstin: str) -> TaxpayerDetails | None:
|
|
336
|
+
blob = self._redis.get(f"gst:{gstin}")
|
|
337
|
+
return TaxpayerDetails.from_payload(json.loads(blob)) if blob else None
|
|
338
|
+
|
|
339
|
+
def set(self, gstin: str, details: TaxpayerDetails) -> None:
|
|
340
|
+
self._redis.setex(f"gst:{gstin}", self._ttl, json.dumps(details.raw))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
client = GSTClient(cache=RedisCache(redis_connection))
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**The client is deliberately not a singleton.** It owns the cookies a captcha
|
|
347
|
+
is bound to, so one shared instance would cross captcha sessions between
|
|
348
|
+
concurrent lookups. The *cache* is the shared piece; clients stay cheap and
|
|
349
|
+
short-lived. The cache stores `.raw`, so a cached entry survives a model
|
|
350
|
+
upgrade.
|
|
351
|
+
|
|
352
|
+
## 7. Error handling
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
GSTValidatorError
|
|
356
|
+
├── InvalidGSTINError (also a ValueError) .value, .reason
|
|
357
|
+
├── CaptchaError captcha could not be fetched
|
|
358
|
+
└── TaxpayerLookupError .code = the portal's errorCode
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from gst_validator import CaptchaError, GSTValidatorError, InvalidGSTINError, TaxpayerLookupError
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
profile = client.fetch_profile(gstin, solved)
|
|
366
|
+
except InvalidGSTINError:
|
|
367
|
+
... # bad input, never hit the network
|
|
368
|
+
except CaptchaError:
|
|
369
|
+
... # portal did not hand out an image
|
|
370
|
+
except TaxpayerLookupError as error:
|
|
371
|
+
if error.code == "SWEB_9000":
|
|
372
|
+
... # wrong or expired captcha - fetch a new one
|
|
373
|
+
except GSTValidatorError:
|
|
374
|
+
... # catch-all for this package
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
The portal answers rejections with HTTP 200 and a body carrying an
|
|
378
|
+
`errorCode`, so the *absence* of `gstin` in the body, not the status code,
|
|
379
|
+
is what marks a failed lookup. One `except GSTValidatorError` catches
|
|
380
|
+
everything this package raises; `httpx` errors are wrapped, never leaked.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
# What you get back
|
|
385
|
+
|
|
386
|
+
### `TaxpayerProfile`
|
|
387
|
+
|
|
388
|
+
| Attribute | Type | Source |
|
|
389
|
+
|---|---|---|
|
|
390
|
+
| `details` | `TaxpayerDetails` | `taxpayerDetails` (captcha) |
|
|
391
|
+
| `goods_and_services` | `tuple[GoodsOrService, ...]` | `goodservice` |
|
|
392
|
+
| `financial_years` | `tuple[FinancialYear, ...]` | `dropdownfinyear` |
|
|
393
|
+
| `filing_preferences` | `tuple[FilingPreference, ...]` | `taxpayerProfileDetails` |
|
|
394
|
+
|
|
395
|
+
Shortcuts: `gstin`, `name`, `is_active`, `as_dict()`.
|
|
396
|
+
|
|
397
|
+
### `TaxpayerDetails`
|
|
398
|
+
|
|
399
|
+
| Attribute | Portal key | Type |
|
|
400
|
+
|---|---|---|
|
|
401
|
+
| `gstin` / `number` | `gstin` | `str` / `GSTIN \| None` |
|
|
402
|
+
| `legal_name` | `lgnm` | `str \| None` |
|
|
403
|
+
| `trade_name` | `tradeNam` | `str \| None` |
|
|
404
|
+
| `name` | (derived) | trade name, else legal name |
|
|
405
|
+
| `status` | `sts` | `str \| None` |
|
|
406
|
+
| `constitution` | `ctb` | `str \| None` |
|
|
407
|
+
| `taxpayer_type` | `dty` | `str \| None` |
|
|
408
|
+
| `registration_date` | `rgdt` | `datetime.date \| None` |
|
|
409
|
+
| `cancellation_date` | `cxdt` | `datetime.date \| None` |
|
|
410
|
+
| `last_updated` | `lstupdt` | `datetime.date \| None` |
|
|
411
|
+
| `nature_of_business` | `nba` | `tuple[str, ...]` |
|
|
412
|
+
| `principal_address` | `pradr` | `Address \| None` |
|
|
413
|
+
| `additional_addresses` | `adadr` | `tuple[Address, ...]` |
|
|
414
|
+
| `central_jurisdiction` | `ctj`, `ctjCd` | `Jurisdiction` |
|
|
415
|
+
| `state_jurisdiction` | `stj`, `stjCd` | `Jurisdiction` |
|
|
416
|
+
| `einvoice_enabled` | `einvoiceStatus` | `bool \| None` |
|
|
417
|
+
| `is_field_visit_conducted` | `isFieldVisitConducted` | `bool \| None` |
|
|
418
|
+
| `core_business_activity` | `ntcrbs` (code expanded) | `str \| None` |
|
|
419
|
+
| `aadhaar_verified` | `adhrVFlag` | `bool \| None` |
|
|
420
|
+
| `aadhaar_verified_on` | `adhrVdt` | `datetime.date \| None` |
|
|
421
|
+
| `ekyc_status` | `ekycVFlag` | `str \| None` |
|
|
422
|
+
| `composition_rate` | `cmpRt` | `str \| None` |
|
|
423
|
+
| `raw` | everything | `dict[str, Any]` |
|
|
424
|
+
|
|
425
|
+
Helpers: `is_active`, `is_cancelled`, `addresses` (principal first),
|
|
426
|
+
`as_dict()`, and `unmapped`, which lists portal keys this class does not model, so a new
|
|
427
|
+
portal field is never silently dropped.
|
|
428
|
+
|
|
429
|
+
`Address` carries split fields (`building_name`, `street`, `pincode`, …) *and*
|
|
430
|
+
`full`: the portal usually sends the principal address as one `adr` string, so
|
|
431
|
+
`as_line()` returns whichever form arrived.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
# Endpoints and what each costs
|
|
436
|
+
|
|
437
|
+
| Method | Endpoint | Captcha? |
|
|
438
|
+
|---|---|---|
|
|
439
|
+
| `fetch_captcha()` | `/services/captcha` | opens the session |
|
|
440
|
+
| `fetch_details()` | `/api/search/taxpayerDetails` | **yes**, one per lookup |
|
|
441
|
+
| `fetch_goods_and_services()` | `/api/search/goodservice` | no |
|
|
442
|
+
| `fetch_financial_years()` | `/api/dropdownfinyear` | no |
|
|
443
|
+
| `fetch_filing_preferences()` | `/api/search/taxpayerProfileDetails` | no |
|
|
444
|
+
| `fetch_profile()` | all of the above | one |
|
|
445
|
+
|
|
446
|
+
`goodservice` returns SAC codes for service providers (`bzsdtls`) and HSN
|
|
447
|
+
codes for goods (`bzgddtls`); both are parsed into `GoodsOrService`, with
|
|
448
|
+
`is_service` telling them apart.
|
|
449
|
+
|
|
450
|
+
The portal fingerprints clients, so the package sends a browser `User-Agent`
|
|
451
|
+
and the `Referer`/`Origin` headers the site expects; without them the captcha
|
|
452
|
+
request is reset. For unattended or high-volume use, the official
|
|
453
|
+
[GST API](https://developer.gst.gov.in/) through a licensed GSP is the
|
|
454
|
+
supported route; this package drives the public, captcha-gated search.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
# Development
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
uv sync # install, including dev dependencies
|
|
462
|
+
uv run pytest # 46 tests, fully offline via httpx.MockTransport
|
|
463
|
+
uv run mypy # strict
|
|
464
|
+
uv run pyright # strict
|
|
465
|
+
uv run ruff check .
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Tests parse payloads with the exact shape the live portal returns
|
|
469
|
+
(`tests/fixtures/`, one service taxpayer and one goods taxpayer, with the
|
|
470
|
+
identifying values replaced by fictional ones) and assert `unmapped == {}`,
|
|
471
|
+
so a portal schema change fails the suite instead of quietly losing data.
|
|
472
|
+
|
|
473
|
+
All GSTINs in this README and in the tests are fictional placeholders built
|
|
474
|
+
on the dummy PAN `AAACR5055K`; they are checksum-valid but belong to nobody.
|
|
475
|
+
|
|
476
|
+
## Releasing
|
|
477
|
+
|
|
478
|
+
CI runs lint, both type checkers, the tests and a build on every push and PR.
|
|
479
|
+
|
|
480
|
+
To publish a release:
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
uv version --bump patch # or minor / major
|
|
484
|
+
git commit -am "Release v$(uv version --short)"
|
|
485
|
+
git tag "v$(uv version --short)"
|
|
486
|
+
git push origin main --tags
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
The tag triggers `.github/workflows/release.yml`, which re-runs the checks,
|
|
490
|
+
builds the sdist and wheel, publishes to PyPI and creates a GitHub release
|
|
491
|
+
with generated notes. The workflow refuses to publish if the tag does not
|
|
492
|
+
match the version in `pyproject.toml`.
|
|
493
|
+
|
|
494
|
+
Publishing uses [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
|
|
495
|
+
(OIDC, no stored secret). One-time setup on PyPI, under
|
|
496
|
+
*Your projects -> Publishing* (or *Pending publishers* for a name that does
|
|
497
|
+
not exist yet):
|
|
498
|
+
|
|
499
|
+
| Field | Value |
|
|
500
|
+
|---|---|
|
|
501
|
+
| PyPI project name | `gst-validator` |
|
|
502
|
+
| Owner | `rahulgurujala` |
|
|
503
|
+
| Repository name | `gst-validator` |
|
|
504
|
+
| Workflow name | `release.yml` |
|
|
505
|
+
| Environment name | `pypi` |
|
|
506
|
+
|
|
507
|
+
If a `PYPI_API_TOKEN` repository secret is set instead, the workflow uses that
|
|
508
|
+
and skips OIDC.
|
|
509
|
+
|
|
510
|
+
## License
|
|
511
|
+
|
|
512
|
+
MIT. See [LICENSE](LICENSE).
|