casparser_isin 2023.9.10__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,43 @@
1
+ # Changelog
2
+
3
+ ## 2023.9.10
4
+ - Fallback to old lookup when direct isin search fails
5
+ - update database
6
+
7
+ ## 2023.9.3
8
+ - Lookup scheme via isin
9
+ - update database
10
+
11
+ ## 2023.8.18
12
+ - fix issues with hdfc mutual fund lookups
13
+ - update database
14
+
15
+ ## 2023.1.16
16
+ - DB updates
17
+
18
+ ## 2021.7.21 - 2021-07-21
19
+ - better support for Franklin Templeton funds
20
+ - support new CAS pdf files after migration of funds from FTAMIL RTA to CAMS
21
+
22
+ ## 2021.7.1 - 2021-07-01
23
+ - add scheme type (`EQUITY`/`DEBT`) to `SchemeData`
24
+ - add nav table for looking up scheme nav for 31-Jan-2018
25
+
26
+ ## 2021.6.1 - 2021-06-01
27
+ - support for using custom isin database via `CASPARSER_ISIN_DB` environment variable.
28
+ - updated isin.db
29
+ - packaging fixes
30
+
31
+ ## 2021.5.1 - 2021-03-02
32
+ - DB updates
33
+ - Essel mutual funds have been renamed to NAVI
34
+ - Dividend options of funds renamed as IDCW
35
+
36
+ ## 2021.4.1 - 2021-04-01
37
+ - updated isin.db
38
+ - updated dependent package versions
39
+
40
+ ## 2021.3.1 - 2021-03-02
41
+ - Switch to calendar versioning
42
+ - Fix bugs with version comparison in cli update tool
43
+ - DB files are hosted in CDN for more frequent updates via CLI. [pypi releases will be limited to major changes in codebase]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Sandeep Somasekharan
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,79 @@
1
+ Metadata-Version: 2.1
2
+ Name: casparser_isin
3
+ Version: 2023.9.10
4
+ Summary: ISIN database for casparser
5
+ Home-page: https://github.com/codereverser/casparser-isin
6
+ License: MIT
7
+ Author: Sandeep Somasekharan
8
+ Author-email: codereverser@gmail.com
9
+ Requires-Python: >=3.8,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Requires-Dist: packaging (>=20.9)
18
+ Requires-Dist: rapidfuzz (>=3.2.0,<4.0.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ # CASParser-ISIN
22
+
23
+ [![code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
24
+ [![GitHub](https://img.shields.io/github/license/codereverser/casparser)](https://github.com/codereverser/casparser/blob/main/LICENSE)
25
+ ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/codereverser/casparser-isin/run-pytest.yml?branch=main)
26
+ [![codecov](https://codecov.io/gh/codereverser/casparser-isin/branch/main/graph/badge.svg?token=MQ8ZEVTG1B)](https://codecov.io/gh/codereverser/casparser-isin)
27
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/casparser-isin)
28
+
29
+ ISIN Database for [casparser](https://github.com/codereverser/casparser).
30
+
31
+ ## Installation
32
+ ```bash
33
+ pip install -U casparser-isin
34
+ ```
35
+
36
+ ## Usage
37
+
38
+
39
+ ```python
40
+ from casparser_isin import MFISINDb
41
+ with MFISINDb() as db:
42
+ scheme_data = db.isin_lookup("Axis Long Term Equity Fund - Growth", # scheme name
43
+ "KFINTECH", # RTA
44
+ "128TSDGG", # Scheme RTA code
45
+ )
46
+ print(scheme_data)
47
+ ```
48
+ ```
49
+ SchemeData(name="axis long term equity fund - direct growth",
50
+ isin="INF846K01EW2",
51
+ amfi_code="120503",
52
+ score=100.0)
53
+ ```
54
+
55
+ The database also contains NAV values on 31-Jan-2018 for all funds, which can be used for
56
+ taxable LTCG computation for units purchased before the same date.
57
+
58
+ ```
59
+ from casparser_isin import MFISINDb
60
+ with MFISINDb() as db:
61
+ nav = db.nav_lookup("INF846K01EW2")
62
+ print(nav)
63
+ ```
64
+ ```
65
+ Decimal('44.8938')
66
+ ```
67
+
68
+
69
+ ## Notes
70
+
71
+ - casparser-isin is shipped with a local database which may get obsolete over time. The local
72
+ database can be updated via the cli tool
73
+
74
+ ```shell
75
+ casparser-isin --update
76
+ ```
77
+
78
+ - casparser-isin will try to use the file provided by `CASPARSER_ISIN_DB` environment variable; if present, and the file exists
79
+
@@ -0,0 +1,58 @@
1
+ # CASParser-ISIN
2
+
3
+ [![code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
4
+ [![GitHub](https://img.shields.io/github/license/codereverser/casparser)](https://github.com/codereverser/casparser/blob/main/LICENSE)
5
+ ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/codereverser/casparser-isin/run-pytest.yml?branch=main)
6
+ [![codecov](https://codecov.io/gh/codereverser/casparser-isin/branch/main/graph/badge.svg?token=MQ8ZEVTG1B)](https://codecov.io/gh/codereverser/casparser-isin)
7
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/casparser-isin)
8
+
9
+ ISIN Database for [casparser](https://github.com/codereverser/casparser).
10
+
11
+ ## Installation
12
+ ```bash
13
+ pip install -U casparser-isin
14
+ ```
15
+
16
+ ## Usage
17
+
18
+
19
+ ```python
20
+ from casparser_isin import MFISINDb
21
+ with MFISINDb() as db:
22
+ scheme_data = db.isin_lookup("Axis Long Term Equity Fund - Growth", # scheme name
23
+ "KFINTECH", # RTA
24
+ "128TSDGG", # Scheme RTA code
25
+ )
26
+ print(scheme_data)
27
+ ```
28
+ ```
29
+ SchemeData(name="axis long term equity fund - direct growth",
30
+ isin="INF846K01EW2",
31
+ amfi_code="120503",
32
+ score=100.0)
33
+ ```
34
+
35
+ The database also contains NAV values on 31-Jan-2018 for all funds, which can be used for
36
+ taxable LTCG computation for units purchased before the same date.
37
+
38
+ ```
39
+ from casparser_isin import MFISINDb
40
+ with MFISINDb() as db:
41
+ nav = db.nav_lookup("INF846K01EW2")
42
+ print(nav)
43
+ ```
44
+ ```
45
+ Decimal('44.8938')
46
+ ```
47
+
48
+
49
+ ## Notes
50
+
51
+ - casparser-isin is shipped with a local database which may get obsolete over time. The local
52
+ database can be updated via the cli tool
53
+
54
+ ```shell
55
+ casparser-isin --update
56
+ ```
57
+
58
+ - casparser-isin will try to use the file provided by `CASPARSER_ISIN_DB` environment variable; if present, and the file exists
@@ -0,0 +1,8 @@
1
+ from .mf_isin import MFISINDb
2
+
3
+ __all__ = [
4
+ "MFISINDb",
5
+ "__version__",
6
+ ]
7
+
8
+ __version__ = "2023.9.10"
@@ -0,0 +1,131 @@
1
+ import argparse
2
+ import logging
3
+ from packaging import version
4
+ import sqlite3
5
+ import sys
6
+ from urllib.error import HTTPError
7
+ from urllib import request
8
+
9
+ from . import __version__
10
+ from .utils import get_isin_db_path
11
+
12
+ META_URL = "https://casparser.atomcoder.com/isin.db.meta"
13
+ DB_URL = "https://casparser.atomcoder.com/isin.db"
14
+
15
+
16
+ def get_metadata():
17
+ conn = sqlite3.connect(get_isin_db_path())
18
+ cursor = conn.cursor()
19
+ try:
20
+ with conn:
21
+ cursor.execute("SELECT key, value from meta")
22
+ metadata = {}
23
+ for key, value in cursor.fetchall():
24
+ if key in ("dbformat", "version"):
25
+ value = version.parse(value)
26
+ metadata[key] = value
27
+ metadata["cli-version"] = version.parse(__version__)
28
+ return metadata
29
+ finally:
30
+ cursor.close()
31
+ conn.close()
32
+
33
+
34
+ def print_version():
35
+ metadata = get_metadata()
36
+ print(f"cli-version : {metadata['cli-version']}")
37
+ print(f"db-version : {metadata['version']}")
38
+ print(f"db-format : {metadata['dbformat']}")
39
+
40
+
41
+ def build_request(url):
42
+ hdr = {
43
+ "User Agent": f"casparser-isin {__version__}",
44
+ "X-origin-casparser": "true",
45
+ }
46
+ return request.Request(url, headers=hdr)
47
+
48
+
49
+ def get_isin_db_details():
50
+ local_meta = get_metadata()
51
+ remote_meta = None
52
+ logging.info("Fetching remote isin db metadata")
53
+ try:
54
+ with request.urlopen(build_request(META_URL)) as response:
55
+ data = response.read().decode()
56
+ except HTTPError as e:
57
+ logging.error("Received error from remote server :: %s", e.reason)
58
+ else:
59
+ remote_meta = {}
60
+ for line in data.splitlines():
61
+ split = line.split("=")
62
+ if len(split) == 2:
63
+ key, value = [x.strip() for x in split]
64
+ if key in ("dbformat", "version"):
65
+ value = version.parse(value)
66
+ remote_meta[key] = value
67
+ logging.info("Local db version : %s", local_meta.get("version"))
68
+ logging.info("Remote db version : %s", remote_meta.get("version"))
69
+ return remote_meta, local_meta
70
+
71
+
72
+ def check_isin_db():
73
+ """Compare remote and local db versions
74
+ Return code:
75
+ 0 - no new database available
76
+ 1 - new database available
77
+ """
78
+ remote_meta, local_meta = get_isin_db_details()
79
+ if (
80
+ remote_meta is not None
81
+ and remote_meta["version"] > local_meta["version"]
82
+ and remote_meta["dbformat"] == local_meta["dbformat"]
83
+ ):
84
+ logging.info("To update the database, re-run the command with --update flag.")
85
+ sys.exit(1)
86
+ else:
87
+ logging.info("Local database is up to date.")
88
+ sys.exit(0)
89
+
90
+
91
+ def update_isin_db():
92
+ remote_meta, local_meta = get_isin_db_details()
93
+ if remote_meta is None:
94
+ return
95
+ elif (
96
+ remote_meta["version"] > local_meta["version"]
97
+ and remote_meta["dbformat"] == local_meta["dbformat"]
98
+ ):
99
+ logging.info("Fetching database version :: %s", remote_meta["version"])
100
+ try:
101
+ with request.urlopen(build_request(DB_URL)) as response:
102
+ data = response.read()
103
+ except HTTPError as e:
104
+ logging.error("Error fetching isin database :: %s", e.reason)
105
+ return
106
+ with open(get_isin_db_path(), "wb") as f:
107
+ f.write(data)
108
+ logging.info("Updated casparser-isin database.")
109
+ else:
110
+ logging.info("casparser-isin database is already upto date")
111
+
112
+
113
+ def main():
114
+ parser = argparse.ArgumentParser("casparser-isin", description="casparser-isin cli")
115
+ parser.add_argument("-v", "--version", help="Print version information", action="store_true")
116
+ parser.add_argument("--update", help="Update isin database", action="store_true")
117
+ parser.add_argument("--check", help="Check remote isin database version", action="store_true")
118
+ args = parser.parse_args()
119
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
120
+ if args.version:
121
+ print_version()
122
+ elif args.update:
123
+ update_isin_db()
124
+ elif args.check:
125
+ check_isin_db()
126
+ else:
127
+ parser.print_help()
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()
@@ -0,0 +1,193 @@
1
+ from collections import namedtuple
2
+ from decimal import Decimal
3
+ import re
4
+ import sqlite3
5
+ from typing import Optional
6
+
7
+ from rapidfuzz import process, utils
8
+
9
+ from .utils import get_isin_db_path
10
+
11
+ RTA_MAP = {
12
+ "CAMS": "CAMS",
13
+ "FTAMIL": "FRANKLIN",
14
+ "FRANKLIN": "FRANKLIN",
15
+ "KFINTECH": "KARVY",
16
+ "KARVY": "KARVY",
17
+ }
18
+
19
+ SchemeData = namedtuple("SchemeData", "name isin amfi_code type score")
20
+
21
+
22
+ def dict_factory(cursor, row):
23
+ d = {}
24
+ for idx, col in enumerate(cursor.description):
25
+ d[col[0]] = row[idx]
26
+ return d
27
+
28
+
29
+ class MFISINDb:
30
+ """ISIN database for (Indian) Mutual Funds."""
31
+
32
+ connection = None
33
+ cursor = None
34
+
35
+ def __enter__(self):
36
+ self.initialize()
37
+ return self
38
+
39
+ def __exit__(self, exc_type, exc_val, exc_tb):
40
+ self.close()
41
+
42
+ def initialize(self):
43
+ """Initialize database."""
44
+ self.connection = sqlite3.connect(get_isin_db_path())
45
+ self.connection.row_factory = dict_factory
46
+ self.cursor = self.connection.cursor()
47
+
48
+ def close(self):
49
+ """Close database connection."""
50
+ if self.cursor is not None:
51
+ self.cursor.close()
52
+ self.cursor = None
53
+ if self.connection is not None:
54
+ self.connection.close()
55
+ self.connection = None
56
+
57
+ def run_query(self, sql, arguments, fetchone=False):
58
+ self_initialized = False
59
+ if self.connection is None:
60
+ self.initialize()
61
+ self_initialized = True
62
+ try:
63
+ self.cursor.execute(sql, arguments)
64
+ if fetchone:
65
+ return self.cursor.fetchone()
66
+ return self.cursor.fetchall()
67
+ finally:
68
+ if self_initialized:
69
+ self.close()
70
+
71
+ def direct_isin_lookup(self, isin: str):
72
+ """
73
+ Lookup scheme data via ISIN code
74
+ :param isin: Fund ISIN
75
+ :return:
76
+ """
77
+ sql = """SELECT name, isin, amfi_code, type from scheme WHERE isin = :isin"""
78
+ return self.run_query(sql, {"isin": isin})
79
+
80
+ def scheme_lookup(self, rta: str, scheme_name: str, rta_code: str):
81
+ """
82
+ Lookup scheme details from the database
83
+ :param rta: RTA (CAMS, KARVY, FTAMIL)
84
+ :param scheme_name: scheme name
85
+ :param rta_code: RTA code for the scheme
86
+ :return:
87
+ """
88
+ if rta_code is not None:
89
+ rta_code = re.sub(r"\s+", "", rta_code)
90
+
91
+ sql = """SELECT name, isin, amfi_code, type from scheme"""
92
+ where = ["rta = :rta"]
93
+
94
+ if re.search(r"fti(\d+)", rta_code, re.I) and rta.upper() in ("CAMS", "FRANKLIN", "FTAMIL"):
95
+ # Try searching db for Franklin schemes
96
+ where_ = ["rta = :rta", "rta_code = :rta_code"]
97
+ args = {"rta": "FRANKLIN", "rta_code": rta_code}
98
+ sql_statement = "{} WHERE {}".format(sql, " AND ".join(where_))
99
+ results = self.run_query(sql_statement, args)
100
+ if len(results) != 0:
101
+ return results
102
+
103
+ args = {"rta": RTA_MAP.get(str(rta).upper(), ""), "rta_code": rta_code}
104
+
105
+ if "hdfc" in scheme_name.lower():
106
+ if re.search("direct", scheme_name, re.I):
107
+ where.append("name LIKE '%direct%'")
108
+ else:
109
+ where.append("name NOT LIKE '%direct%'")
110
+
111
+ if re.search("dividend|idcw", scheme_name, re.I):
112
+ if re.search("re-*invest", scheme_name, re.I):
113
+ where.append("name LIKE '%reinvest%'")
114
+ else:
115
+ where.append("name LIKE '%payout%'")
116
+ where.append("rta_code like :rta_code_d")
117
+ args.update(rta_code_d=f"{rta_code}%")
118
+ else:
119
+ where.append("rta_code = :rta_code")
120
+
121
+ sql_statement = "{} WHERE {} GROUP BY isin".format(sql, " AND ".join(where))
122
+ results = self.run_query(sql_statement, args)
123
+ if len(results) == 0 and "rta_code" in args:
124
+ args["rta_code"] = args["rta_code"][:-1]
125
+ results = self.run_query(sql_statement, args)
126
+ return results
127
+
128
+ def isin_lookup(
129
+ self,
130
+ scheme_name: str,
131
+ rta: str,
132
+ rta_code: str,
133
+ isin: Optional[str] = None,
134
+ min_score: int = 60,
135
+ ) -> SchemeData:
136
+ """
137
+ Return the closest matching scheme from MF isin database.
138
+
139
+ :param scheme_name: Scheme Name
140
+ :param rta: RTA (CAMS, KARVY, KFINTECH)
141
+ :param rta_code: Scheme RTA code
142
+ :param isin: Fund ISIN
143
+ :param min_score: Minimum score (out of 100) required from the fuzzy match algorithm
144
+
145
+ :return: isin and amfi_code code for matching scheme.
146
+ :rtype: SchemeData
147
+ :raises: ValueError if no scheme is found in the database.
148
+ """
149
+
150
+ if not (
151
+ isinstance(scheme_name, str) and isinstance(rta, str) and isinstance(rta_code, str)
152
+ ):
153
+ raise TypeError("Invalid input")
154
+ if rta.upper() not in RTA_MAP:
155
+ raise ValueError(f"Invalid RTA : {rta}")
156
+ results = []
157
+ if isin is not None:
158
+ results = self.direct_isin_lookup(isin)
159
+ if len(results) == 0:
160
+ results = self.scheme_lookup(rta, scheme_name, rta_code)
161
+ if len(results) == 1:
162
+ result = results[0]
163
+ return SchemeData(
164
+ name=result["name"],
165
+ isin=result["isin"],
166
+ amfi_code=result["amfi_code"],
167
+ type=result["type"],
168
+ score=100,
169
+ )
170
+ elif len(results) > 1:
171
+ schemes = {
172
+ x["name"]: (x["name"], x["isin"], x["amfi_code"], x["type"]) for x in results
173
+ }
174
+ key, score, _ = process.extractOne(
175
+ scheme_name, schemes.keys(), processor=utils.default_process
176
+ )
177
+ if score >= min_score:
178
+ name, isin, amfi_code, scheme_type = schemes[key]
179
+ return SchemeData(
180
+ name=name, isin=isin, amfi_code=amfi_code, type=scheme_type, score=score
181
+ )
182
+ raise ValueError("No schemes found")
183
+
184
+ def nav_lookup(self, isin: str) -> Optional[Decimal]:
185
+ """
186
+ Return the NAV of the fund on 31st Jan 2018. used for LTCG computations
187
+ :param isin: Fund ISIN
188
+ :return: nav value as a Decimal if available, else return None
189
+ """
190
+ sql = """SELECT nav FROM nav20180131 where isin = :isin"""
191
+ result = self.run_query(sql, {"isin": isin}, fetchone=True)
192
+ if result is not None:
193
+ return Decimal(result["nav"])
@@ -0,0 +1,15 @@
1
+ import os
2
+ import pathlib
3
+
4
+ BASE_DIR = pathlib.Path(__file__).resolve().parent
5
+ INTERNAL_ISIN_DB_PATH = BASE_DIR / "isin.db"
6
+
7
+
8
+ def get_isin_db_path():
9
+ env_isin_path = os.getenv("CASPARSER_ISIN_DB")
10
+ try:
11
+ if os.path.exists(env_isin_path) and os.path.isfile(env_isin_path):
12
+ return pathlib.Path(env_isin_path)
13
+ except TypeError:
14
+ pass
15
+ return INTERNAL_ISIN_DB_PATH
@@ -0,0 +1,58 @@
1
+ [tool.poetry]
2
+ name = "casparser_isin"
3
+ version = "0"
4
+ description = "ISIN database for casparser"
5
+ authors = ["Sandeep Somasekharan <codereverser@gmail.com>"]
6
+ homepage = "https://github.com/codereverser/casparser-isin"
7
+ license = "MIT License"
8
+ readme = "README.md"
9
+ classifiers = [
10
+ "License :: OSI Approved :: MIT License",
11
+ "Programming Language :: Python :: 3.8",
12
+ "Programming Language :: Python :: 3.9",
13
+ "Programming Language :: Python :: 3.10",
14
+ "Operating System :: OS Independent"
15
+ ]
16
+ include = [ "CHANGELOG.md" ]
17
+
18
+ [tool.poetry.dependencies]
19
+ python = "^3.8"
20
+ packaging = ">=20.9"
21
+ rapidfuzz = "^3.2.0"
22
+
23
+ [tool.poetry.dev-dependencies]
24
+ coverage = {version = "^7.3.0", extras=["toml"]}
25
+ pytest = "^7.4.0"
26
+ pytest-cov = "^4.1.0"
27
+ apsw = "^3.43.0"
28
+ b2sdk = "^1.24.0"
29
+ lxml = "^4.9.0"
30
+ python-dotenv = "^1.0.0"
31
+ requests = "^2.31.0"
32
+ requests-cache = "^1.1.0"
33
+ pre-commit = "^3.4.0"
34
+
35
+ [tool.poetry.scripts]
36
+ casparser-isin = "casparser_isin.cli:main"
37
+
38
+ [build-system]
39
+ requires = ["poetry-core >= 1.0.0"]
40
+ build-backend = "poetry.core.masonry.api"
41
+
42
+ [tool.black]
43
+ line-length = 100
44
+ target-version = ["py38"]
45
+
46
+ [tool.pytest.ini_options]
47
+ minversion = "6.0"
48
+ addopts = "--cov=casparser_isin --cov-config=tox.ini --cov-report=xml --cov-report=html --exitfirst"
49
+ testpaths = [
50
+ "tests",
51
+ ]
52
+
53
+ [tool.ruff]
54
+ line-length = 100
55
+ target-version = "py38"
56
+
57
+ [tool.poetry-version-plugin]
58
+ source = "init"