Skip to content

Commit fbbdf7f

Browse files
authored
Merge pull request #103 from codereverser/feature/cdsl-ndsl-parser
NSDL support: alpha version
2 parents 6eaa108 + 54e213f commit fbbdf7f

File tree

19 files changed

+1751
-1423
lines changed

19 files changed

+1751
-1423
lines changed

.github/workflows/pypi-publish.yml

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,19 @@ on:
77
jobs:
88
deploy:
99
environment: pypi
10+
permissions:
11+
id-token: write
1012
runs-on: ubuntu-latest
1113

1214
steps:
13-
- uses: actions/checkout@v3
14-
- name: Install poetry
15-
run: pipx install poetry
15+
- uses: actions/checkout@v4
1616
- name: Set up Python
17-
uses: actions/setup-python@v4
17+
uses: actions/setup-python@v5
1818
with:
19-
python-version: '3.8'
20-
- name: Build and publish
21-
env:
22-
PYPI_TOKEN: ${{ secrets.PYPI_PASSWORD }}
23-
run: |
24-
poetry self add poetry-version-plugin
25-
poetry config pypi-token.pypi $PYPI_TOKEN
26-
poetry publish --build
19+
python-version: '3.10'
20+
- name: Install uv
21+
uses: astral-sh/setup-uv@v5
22+
- name: Build
23+
run: uv build
24+
- name: Publish
25+
run: uv publish

.github/workflows/run-pytest.yml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ name: run-tests
22

33
on:
44
push:
5+
branches:
6+
- '*'
7+
tags-ignore:
8+
- 'v*'
59
pull_request:
610
branches:
711
- main
@@ -14,22 +18,21 @@ jobs:
1418
python-version: ['3.10']
1519

1620
steps:
17-
- uses: actions/checkout@v3
18-
- name: Install poetry
19-
run: pipx install poetry
21+
- uses: actions/checkout@v4
2022
- name: Set up Python ${{ matrix.python-version }}
21-
uses: actions/setup-python@v4
23+
uses: actions/setup-python@v5
2224
with:
2325
python-version: ${{ matrix.python-version }}
24-
cache: 'poetry'
26+
- name: Install uv
27+
uses: astral-sh/setup-uv@v5
28+
- name: Install dependencies
29+
run: uv sync --all-extras --dev
2530
- name: Extract test files
2631
run: ./.github/scripts/extract_files.sh
2732
env:
2833
FILES_PASSPHRASE: ${{ secrets.FILES_PASSPHRASE }}
29-
- name: Install dependencies
30-
run: poetry install -E mupdf
3134
- name: Test with pytest
32-
run: poetry run pytest
35+
run: uv run pytest
3336
env:
3437
BAD_CAS_FILE: ${{ secrets.BAD_CAS_FILE }}
3538
CAMS_CAS_FILE: ${{ secrets.CAMS_CAS_FILE }}
@@ -40,6 +43,7 @@ jobs:
4043
KFINTECH_CAS_FILE: ${{ secrets.KFINTECH_CAS_FILE }}
4144
KFINTECH_CAS_FILE_NEW: ${{ secrets.KFINTECH_CAS_FILE_NEW }}
4245
KFINTECH_CAS_PASSWORD: ${{ secrets.KFINTECH_CAS_PASSWORD }}
46+
NSDL_CAS_FILE_1: ${{ secrets.NSDL_CAS_FILE_1 }}
4347
- name: Upload coverage report to codecov
4448
uses: codecov/codecov-action@v5
4549
with:

casparser/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
"CapitalGainsReport",
1010
]
1111

12-
__version__ = "0.7.4"
12+
__version__ = "0.8.0"

casparser/cli.py

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515

1616
from . import __version__, read_cas_pdf
1717
from .analysis.gains import CapitalGainsReport
18-
from .enums import CASFileType
18+
from .enums import CASFileType, FileType
1919
from .exceptions import GainsError, IncompleteCASError, ParserException
2020
from .parsers.utils import cas2csv, cas2csv_summary, cas2json, is_close
21-
from .types import CASData
21+
from .types import CASData, NSDLCASData
2222

2323
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
2424
console = Console()
@@ -55,6 +55,99 @@ def get_color(amount: Union[Decimal, float, int]):
5555
return "white"
5656

5757

58+
def print_nsdl(parsed_data: NSDLCASData):
59+
"""Print summary of parsed data."""
60+
61+
count = 0
62+
err = 0
63+
64+
data = parsed_data.model_dump(by_alias=True)
65+
# console.print(data)
66+
67+
summary_table = Table.grid(expand=True)
68+
summary_table.add_column(justify="right")
69+
summary_table.add_column(justify="left")
70+
spacing = (0, 1)
71+
summary_table.add_row(
72+
Padding("Statement Period :", spacing),
73+
f"[bold green]{data['statement_period']['from']}[/] To "
74+
f"[bold green]{data['statement_period']['to']}[/]",
75+
)
76+
summary_table.add_row(Padding("File Type :", spacing), f"[bold]{data['file_type']}[/]")
77+
# summary_table.add_row(Padding("CAS Type :", spacing), f"[bold]{data['cas_type']}[/]")
78+
for key, value in data["investor_info"].items():
79+
summary_table.add_row(
80+
Padding(f"{key.capitalize()} :", spacing), re.sub(r"[^\S\r\n]+", " ", value)
81+
)
82+
console.print(summary_table)
83+
console.print("")
84+
85+
table = Table(title="Portfolio Summary", show_lines=True)
86+
table.add_column("Name")
87+
table.add_column("ISIN")
88+
table.add_column("Units")
89+
table.add_column("Price")
90+
table.add_column("Value")
91+
92+
value = Decimal(0)
93+
94+
for account in parsed_data.accounts:
95+
balance = account.balance
96+
value += balance
97+
running_balance = 0
98+
table_rows = []
99+
if len(account.equities) > 0:
100+
table_rows.append(["[italic]Equities[/]"])
101+
for equity in account.equities:
102+
running_balance += equity.num_shares * equity.price
103+
table_rows.append(
104+
[
105+
equity.name,
106+
equity.isin,
107+
format_number(equity.num_shares),
108+
formatINR(equity.price),
109+
formatINR(equity.value),
110+
]
111+
)
112+
if len(account.mutual_funds) > 0:
113+
table_rows.append(["[italic]Mutual Funds[/]"])
114+
for mf in account.mutual_funds:
115+
running_balance += mf.nav * mf.balance
116+
table_rows.append(
117+
[
118+
mf.name,
119+
mf.isin,
120+
format_number(mf.balance),
121+
formatINR(mf.nav),
122+
formatINR(mf.value),
123+
]
124+
)
125+
if is_close(balance, running_balance, tol=float(balance or 1) * 0.01):
126+
status = "️✅"
127+
else:
128+
status = "❗️"
129+
err += 1
130+
count += 1
131+
table.add_row(
132+
f"[bold]{account.name}\n{account.dp_id} - {account.client_id}[/]", "", "", "", status
133+
)
134+
135+
for row in table_rows:
136+
table.add_row(*row)
137+
138+
console.print(table)
139+
140+
console.print(
141+
f"Portfolio Valuation : [bold green]{formatINR(value)}[/] "
142+
f"[As of {data['statement_period']['to']}]"
143+
)
144+
145+
console.print("[bold]Summary[/]")
146+
console.print(f"{'Total':8s}: [bold white]{count:4d}[/] accounts")
147+
console.print(f"{'Matched':8s}: [bold green]{count - err:4d}[/] accounts")
148+
console.print(f"{'Error':8s}: [bold red]{err:4d}[/] accounts")
149+
150+
58151
def print_summary(parsed_data: CASData, output_filename=None, include_zero_folios=False):
59152
"""Print summary of parsed data."""
60153
count = 0
@@ -348,15 +441,20 @@ def cli(output, summary, password, include_all, gains, gains_112a, force_pdfmine
348441
except ParserException as exc:
349442
console.print(f"Error parsing pdf file :: [bold red]{str(exc)}[/]")
350443
sys.exit(1)
351-
if summary:
444+
if isinstance(data, NSDLCASData):
445+
print_nsdl(data)
446+
elif summary:
352447
print_summary(
353448
data,
354449
include_zero_folios=include_all,
355450
output_filename=None if output_ext in (".csv", ".json") else output,
356451
)
357452

358453
if output_ext in (".csv", ".json"):
359-
if output_ext == ".csv":
454+
if output_ext == ".csv" and data.file_type in (
455+
FileType.CAMS.value,
456+
FileType.KFINTECH.value,
457+
):
360458
if summary or data.cas_type == CASFileType.SUMMARY.name:
361459
description = "Generating summary CSV file..."
362460
conv_fn = cas2csv_summary
@@ -370,7 +468,7 @@ def cli(output, summary, password, include_all, gains, gains_112a, force_pdfmine
370468
with open(output, "w", newline="", encoding="utf-8") as fp:
371469
fp.write(conv_fn(data))
372470
console.print(f"File saved : [bold]{output}[/]")
373-
if gains or gains_112a:
471+
if data.file_type in (FileType.CAMS.value, FileType.KFINTECH.value) and (gains or gains_112a):
374472
try:
375473
print_gains(
376474
data,

casparser/enums.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class FileType(AutoEnum):
1717
UNKNOWN = auto()
1818
CAMS = auto()
1919
KFINTECH = auto()
20+
CDSL = auto()
21+
NSDL = auto()
2022

2123

2224
class CASFileType(AutoEnum):

casparser/parsers/__init__.py

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Union
33

44
from casparser.process import process_cas_text
5-
from casparser.types import CASData
5+
from casparser.types import CASData, NSDLCASData, ProcessedCASData
66

77
from .utils import cas2csv, cas2json
88

@@ -32,31 +32,39 @@ def read_cas_pdf(
3232
from .pdfminer import cas_pdf_to_text
3333

3434
partial_cas_data = cas_pdf_to_text(filename, password)
35-
36-
processed_data = process_cas_text("\u2029".join(partial_cas_data.lines))
37-
38-
if sort_transactions:
39-
for folio in processed_data.folios:
40-
for idx, scheme in enumerate(folio.schemes):
41-
dates = [x.date for x in scheme.transactions]
42-
sorted_dates = list(sorted(dates))
43-
if dates != sorted_dates:
44-
sorted_transactions = []
45-
balance = scheme.open
46-
for transaction in sorted(scheme.transactions, key=lambda x: x.date):
47-
balance += transaction.units or 0
48-
transaction.balance = balance
49-
sorted_transactions.append(transaction)
50-
scheme.transactions = sorted_transactions
51-
folio.schemes[idx] = scheme
52-
53-
final_data = CASData(
54-
statement_period=processed_data.statement_period,
55-
folios=processed_data.folios,
56-
investor_info=partial_cas_data.investor_info,
57-
cas_type=processed_data.cas_type,
58-
file_type=partial_cas_data.file_type,
35+
processed_data = process_cas_text(
36+
"\u2029".join(partial_cas_data.lines), partial_cas_data.file_type
5937
)
38+
if isinstance(processed_data, ProcessedCASData):
39+
if sort_transactions:
40+
for folio in processed_data.folios:
41+
for idx, scheme in enumerate(folio.schemes):
42+
dates = [x.date for x in scheme.transactions]
43+
sorted_dates = list(sorted(dates))
44+
if dates != sorted_dates:
45+
sorted_transactions = []
46+
balance = scheme.open
47+
for transaction in sorted(scheme.transactions, key=lambda x: x.date):
48+
balance += transaction.units or 0
49+
transaction.balance = balance
50+
sorted_transactions.append(transaction)
51+
scheme.transactions = sorted_transactions
52+
folio.schemes[idx] = scheme
53+
54+
final_data = CASData(
55+
statement_period=processed_data.statement_period,
56+
folios=processed_data.folios,
57+
investor_info=partial_cas_data.investor_info,
58+
cas_type=processed_data.cas_type,
59+
file_type=partial_cas_data.file_type,
60+
)
61+
else:
62+
final_data = NSDLCASData(
63+
statement_period=processed_data.statement_period,
64+
accounts=processed_data.accounts,
65+
investor_info=partial_cas_data.investor_info,
66+
file_type=partial_cas_data.file_type,
67+
)
6068
if output == "dict":
6169
return final_data
6270
elif output == "csv":

0 commit comments

Comments
 (0)