Skip to content

Commit f44c2f6

Browse files
authored
Use pydantic models (#64)
* use pydantic * fix test cases * v0.6.0
1 parent 2396be8 commit f44c2f6

18 files changed

+473
-372
lines changed

.pre-commit-config.yaml

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
# See https://pre-commit.com for more information
2-
# See https://pre-commit.com/hooks.html for more hooks
31
repos:
4-
- repo: https://github.com/pre-commit/pre-commit-hooks
5-
rev: v4.1.0
2+
- repo: 'https://github.com/pre-commit/pre-commit-hooks'
3+
rev: v4.4.0
64
hooks:
7-
- id: trailing-whitespace
8-
- id: end-of-file-fixer
9-
- id: check-yaml
10-
- id: check-added-large-files
11-
- repo: https://github.com/pycqa/flake8
12-
rev: '4.0.1'
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: check-yaml
8+
- id: check-added-large-files
9+
- repo: 'https://github.com/pycqa/flake8'
10+
rev: 6.0.0
1311
hooks:
14-
- id: flake8
15-
- repo: https://github.com/ambv/black
16-
rev: 22.1.0
12+
- id: flake8
13+
- repo: 'https://github.com/psf/black'
14+
rev: 23.1.0
1715
hooks:
18-
- id: black
19-
language_version: python3.10
16+
- id: black
17+
language_version: python3.11
18+
- repo: 'https://github.com/pycqa/isort'
19+
rev: 5.12.0
20+
hooks:
21+
- id: isort
22+
args: ["--profile", "black", "--filter-files"]
23+
name: isort (python)

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## 0.6.0 - 2023-02-20
4+
- use pydantic models for better data validation
5+
36
## 0.5.5 - 2022-08-06
47
- bug fix with MuPDF parser
58

casparser/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from .parsers import read_cas_pdf
2-
from .analysis import CapitalGainsReport
3-
from .types import CASParserDataType
41
from .__version__ import __version__
2+
from .analysis import CapitalGainsReport
3+
from .parsers import read_cas_pdf
4+
from .types import CASData
55

66
__all__ = [
77
"read_cas_pdf",
88
"__version__",
9-
"CASParserDataType",
9+
"CASData",
1010
"CapitalGainsReport",
1111
]

casparser/analysis/gains.py

Lines changed: 49 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
import csv
2+
import io
3+
import itertools
24
from collections import deque
35
from dataclasses import dataclass
4-
from decimal import Decimal
56
from datetime import date
6-
import io
7-
import itertools
7+
from decimal import Decimal
88
from typing import List, Optional
99

1010
from dateutil.parser import parse as dateparse
1111
from dateutil.relativedelta import relativedelta
1212

13-
from casparser.exceptions import IncompleteCASError, GainsError
1413
from casparser.enums import FundType, GainType, TransactionType
15-
from casparser.types import CASParserDataType, TransactionDataType
14+
from casparser.exceptions import GainsError, IncompleteCASError
15+
from casparser.types import CASData, TransactionData
16+
1617
from .utils import CII, get_fin_year, nav_search
1718

1819
PURCHASE_TXNS = {
19-
TransactionType.DIVIDEND_REINVEST.name,
20-
TransactionType.PURCHASE.name,
21-
TransactionType.PURCHASE_SIP.name,
22-
TransactionType.REVERSAL.name,
20+
TransactionType.DIVIDEND_REINVEST,
21+
TransactionType.PURCHASE,
22+
TransactionType.PURCHASE_SIP,
23+
TransactionType.REVERSAL,
2324
# Segregated folios are not supported
2425
# TransactionType.SEGREGATION.name,
25-
TransactionType.SWITCH_IN.name,
26-
TransactionType.SWITCH_IN_MERGER.name,
26+
TransactionType.SWITCH_IN,
27+
TransactionType.SWITCH_IN_MERGER,
2728
}
2829

2930
SALE_TXNS = {
@@ -87,25 +88,25 @@ class MergedTransaction:
8788
stt: Decimal = Decimal(0.0)
8889
tds: Decimal = Decimal(0.0)
8990

90-
def add(self, txn: TransactionDataType):
91-
txn_type = txn["type"]
92-
if txn_type in PURCHASE_TXNS and txn["units"] is not None:
93-
self.nav = txn["nav"]
94-
self.purchase_units += txn["units"]
95-
self.purchase += txn["amount"]
96-
elif txn_type in SALE_TXNS and txn["units"] is not None:
97-
self.nav = txn["nav"]
98-
self.sale_units += txn["units"]
99-
self.sale += txn["amount"]
100-
elif txn_type == TransactionType.STT_TAX.name:
101-
self.stt += txn["amount"]
102-
elif txn_type == TransactionType.STAMP_DUTY_TAX.name:
103-
self.stamp_duty += txn["amount"]
104-
elif txn_type == TransactionType.TDS_TAX.name:
105-
self.tds += txn["amount"]
106-
elif txn_type == TransactionType.SEGREGATION.name:
91+
def add(self, txn: TransactionData):
92+
txn_type = txn.type
93+
if txn_type in PURCHASE_TXNS and txn.units is not None:
94+
self.nav = txn.nav
95+
self.purchase_units += txn.units
96+
self.purchase += txn.amount
97+
elif txn_type in SALE_TXNS and txn.units is not None:
98+
self.nav = txn.nav
99+
self.sale_units += txn.units
100+
self.sale += txn.amount
101+
elif txn_type == TransactionType.STT_TAX:
102+
self.stt += txn.amount
103+
elif txn_type == TransactionType.STAMP_DUTY_TAX:
104+
self.stamp_duty += txn.amount
105+
elif txn_type == TransactionType.TDS_TAX:
106+
self.tds += txn.amount
107+
elif txn_type == TransactionType.SEGREGATION:
107108
self.nav = Decimal(0.0)
108-
self.purchase_units += txn["units"]
109+
self.purchase_units += txn.units
109110
self.purchase = Decimal(0.0)
110111

111112

@@ -186,7 +187,7 @@ def index_ratio(self) -> Decimal:
186187

187188
@property
188189
def coa(self) -> Decimal:
189-
if self.fund.type == FundType.DEBT.name:
190+
if self.fund.type == FundType.DEBT:
190191
return Decimal(round(self.purchase_value * self.index_ratio, 2))
191192
if self.purchase_date < self.__cutoff_date:
192193
if self.sale_date < self.__sell_cutoff_date:
@@ -213,7 +214,7 @@ def stcg(self) -> Decimal:
213214
return Decimal(0.0)
214215

215216

216-
def get_fund_type(transactions: List[TransactionDataType]) -> FundType:
217+
def get_fund_type(transactions: List[TransactionData]) -> FundType:
217218
"""
218219
Detect Fund Type.
219220
- UNKNOWN if there are no redemption transactions
@@ -225,23 +226,23 @@ def get_fund_type(transactions: List[TransactionDataType]) -> FundType:
225226
"""
226227
valid = any(
227228
[
228-
x["units"] is not None and x["units"] < 0 and x["type"] != TransactionType.REVERSAL.name
229+
x.units is not None and x.units < 0 and x.type != TransactionType.REVERSAL
229230
for x in transactions
230231
]
231232
)
232233
if not valid:
233234
return FundType.UNKNOWN
234235
return (
235236
FundType.EQUITY
236-
if any([x["type"] == TransactionType.STT_TAX.name for x in transactions])
237+
if any([x.type == TransactionType.STT_TAX for x in transactions])
237238
else FundType.DEBT
238239
)
239240

240241

241242
class FIFOUnits:
242243
"""First-In First-Out units calculator."""
243244

244-
def __init__(self, fund: Fund, transactions: List[TransactionDataType]):
245+
def __init__(self, fund: Fund, transactions: List[TransactionData]):
245246
"""
246247
:param fund: name of fund, mainly for reporting purposes.
247248
:param transactions: list of transactions for the fund
@@ -264,13 +265,13 @@ def __init__(self, fund: Fund, transactions: List[TransactionDataType]):
264265
@property
265266
def clean_transactions(self):
266267
"""remove redundant transactions, without amount"""
267-
return filter(lambda x: x["amount"] is not None, self._original_transactions)
268+
return filter(lambda x: x.amount is not None, self._original_transactions)
268269

269270
def merge_transactions(self):
270271
"""Group transactions by date with taxes and investments/redemptions separated."""
271272
merged_transactions = {}
272-
for txn in sorted(self.clean_transactions, key=lambda x: (x["date"], -x["amount"])):
273-
dt = txn["date"]
273+
for txn in sorted(self.clean_transactions, key=lambda x: (x.date, -x.amount)):
274+
dt = txn.date
274275

275276
if isinstance(dt, str):
276277
dt = dateparse(dt).date()
@@ -347,8 +348,8 @@ def sell(self, sell_date: date, quantity: Decimal, nav: Decimal, tax: Decimal):
347348
class CapitalGainsReport:
348349
"""Generate Capital Gains Report from the parsed CAS data"""
349350

350-
def __init__(self, data: CASParserDataType):
351-
self._data: CASParserDataType = data
351+
def __init__(self, data: CASData):
352+
self._data: CASData = data
352353
self._gains: List[GainEntry] = []
353354
self.errors = []
354355
self.invested_amount = Decimal(0.0)
@@ -370,25 +371,25 @@ def get_fy_list(self) -> List[str]:
370371

371372
def process_data(self):
372373
self._gains = []
373-
for folio in self._data.get("folios", []):
374-
for scheme in folio.get("schemes", []):
375-
transactions = scheme["transactions"]
374+
for folio in self._data.folios:
375+
for scheme in folio.schemes:
376+
transactions = scheme.transactions
376377
fund = Fund(
377-
scheme=scheme["scheme"],
378-
folio=folio["folio"],
379-
isin=scheme["isin"],
380-
type=scheme["type"],
378+
scheme=scheme.scheme,
379+
folio=folio.folio,
380+
isin=scheme.isin,
381+
type=scheme.type,
381382
)
382383
if len(transactions) > 0:
383-
if scheme["open"] >= 0.01:
384+
if scheme.open >= 0.01:
384385
raise IncompleteCASError(
385386
"Incomplete CAS found. For gains computation, "
386387
"all folios should have zero opening balance"
387388
)
388389
try:
389390
fifo = FIFOUnits(fund, transactions)
390391
self.invested_amount += fifo.invested
391-
self.current_value += scheme["valuation"]["value"]
392+
self.current_value += scheme.valuation.value
392393
self._gains.extend(fifo.gains)
393394
except GainsError as exc:
394395
self.errors.append((fund.name, str(exc)))

casparser/cli.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1-
from decimal import Decimal
21
import itertools
32
import os
43
import re
54
import sys
5+
from decimal import Decimal
66
from typing import Union
77

88
import click
99
from rich.console import Console
1010
from rich.markdown import Markdown
1111
from rich.padding import Padding
12-
from rich.progress import BarColumn, TextColumn, SpinnerColumn, Progress
13-
from rich.prompt import Confirm, Prompt
12+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
13+
from rich.prompt import Prompt
1414
from rich.table import Table
1515

16-
from .__version__ import __version__
17-
1816
from . import read_cas_pdf
17+
from .__version__ import __version__
1918
from .analysis.gains import CapitalGainsReport
2019
from .enums import CASFileType
21-
from .exceptions import ParserException, IncompleteCASError, GainsError
22-
from .parsers.utils import is_close, cas2json, cas2csv, cas2csv_summary
20+
from .exceptions import GainsError, IncompleteCASError, ParserException
21+
from .parsers.utils import cas2csv, cas2csv_summary, cas2json, is_close
22+
from .types import CASData
2323

2424
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
2525
console = Console()
@@ -38,11 +38,12 @@ def get_color(amount: Union[Decimal, float, int]):
3838
return "white"
3939

4040

41-
def print_summary(data, output_filename=None, include_zero_folios=False):
41+
def print_summary(parsed_data: CASData, output_filename=None, include_zero_folios=False):
4242
"""Print summary of parsed data."""
4343
count = 0
4444
err = 0
4545

46+
data = parsed_data.dict(by_alias=True)
4647
is_summary = data["cas_type"] == CASFileType.SUMMARY.name
4748

4849
# Print CAS header stuff
@@ -92,7 +93,6 @@ def print_summary(data, output_filename=None, include_zero_folios=False):
9293
folio_header_added = False
9394
current_amc = folio["amc"]
9495
for scheme in folio["schemes"]:
95-
9696
if scheme["close"] < 1e-3 and not include_zero_folios:
9797
continue
9898

@@ -130,7 +130,7 @@ def print_summary(data, output_filename=None, include_zero_folios=False):
130130
count += 1
131131

132132
table = Table(title="Portfolio Summary", show_lines=True)
133-
for (hdr, align) in zip(console_header.values(), console_col_align):
133+
for hdr, align in zip(console_header.values(), console_col_align):
134134
# noinspection PyTypeChecker
135135
table.add_column(hdr, justify=align)
136136
for row in console_rows:
@@ -152,9 +152,9 @@ def print_summary(data, output_filename=None, include_zero_folios=False):
152152
console.print(f"File saved : [bold]{output_filename}[/]")
153153

154154

155-
def print_gains(data, output_file_path=None, gains_112a=""):
156-
cg = CapitalGainsReport(data)
157-
155+
def print_gains(parsed_data: CASData, output_file_path=None, gains_112a=""):
156+
cg = CapitalGainsReport(parsed_data)
157+
data = parsed_data.dict(by_alias=True)
158158
if not cg.has_gains():
159159
console.print("[bold yellow]Warning:[/] No capital gains info found in CAS")
160160
return
@@ -325,7 +325,7 @@ def cli(output, summary, password, include_all, gains, gains_112a, force_pdfmine
325325

326326
if output_ext in (".csv", ".json"):
327327
if output_ext == ".csv":
328-
if summary or data["cas_type"] == CASFileType.SUMMARY.name:
328+
if summary or data.cas_type == CASFileType.SUMMARY.name:
329329
description = "Generating summary CSV file..."
330330
conv_fn = cas2csv_summary
331331
else:

casparser/encoder.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)