bootlick 0.2.2__tar.gz → 0.2.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bootlick
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A package for specific websites
5
5
  Author-email: Unknown <unknown@example.com>
6
6
  License-Expression: MIT
@@ -62,6 +62,149 @@ def get_dynamic_config():
62
62
  return get_config()
63
63
 
64
64
 
65
+ # Canonical columns written to {date}_mf.csv. Process mode and historical files depend on these names.
66
+ _AMFI_NAV_OUTPUT_COLUMNS = [
67
+ "code",
68
+ "scheme",
69
+ "isin_growth_dividend",
70
+ "isin_reinvestment",
71
+ "nav",
72
+ "repurchase_price",
73
+ "sale_price",
74
+ "date",
75
+ "category",
76
+ ]
77
+
78
+
79
+ def _amfi_colmap(columns):
80
+ return {str(c).strip().lower(): c for c in columns}
81
+
82
+
83
+ def _amfi_resolve_col(colmap, *aliases):
84
+ for alias in aliases:
85
+ if alias in colmap:
86
+ return colmap[alias]
87
+ return None
88
+
89
+
90
+ def _is_amfi_isin(value):
91
+ """True for AMFI scheme ISINs (INF...), not Plan/Option text like 'INSTITUTIONAL...'."""
92
+ if not pd.notna(value):
93
+ return False
94
+ text = str(value).strip()
95
+ return len(text) >= 12 and text.startswith("IN") and " " not in text
96
+
97
+
98
+ def _join_amfi_scheme(name, plan, option):
99
+ parts = []
100
+ for value in (name, plan, option):
101
+ if pd.notna(value):
102
+ text = str(value).strip()
103
+ if text:
104
+ parts.append(text)
105
+ return " - ".join(parts) if parts else pd.NA
106
+
107
+
108
+ def _canonicalize_amfi_nav_frame(data: pd.DataFrame) -> pd.DataFrame:
109
+ """Map old (Repurchase/Sale) and new (Plan/Option) AMFI headers to one schema."""
110
+ colmap = _amfi_colmap(data.columns)
111
+ code_col = _amfi_resolve_col(colmap, "scheme code", "code")
112
+ name_col = _amfi_resolve_col(colmap, "scheme name", "nav name", "scheme")
113
+ isin_growth_col = _amfi_resolve_col(
114
+ colmap, "isin div payout/isin growth", "isin growth", "isin div payout"
115
+ )
116
+ isin_reinv_col = _amfi_resolve_col(colmap, "isin div reinvestment")
117
+ nav_col = _amfi_resolve_col(colmap, "net asset value", "nav")
118
+ date_col = _amfi_resolve_col(colmap, "date")
119
+ required = {
120
+ "scheme code": code_col,
121
+ "scheme name": name_col,
122
+ "isin div payout/isin growth": isin_growth_col,
123
+ "isin div reinvestment": isin_reinv_col,
124
+ "net asset value": nav_col,
125
+ "date": date_col,
126
+ }
127
+ missing = [name for name, col in required.items() if col is None]
128
+ if missing:
129
+ raise ValueError(
130
+ f"Unrecognized AMFI NAV header, missing: {missing}; got: {[str(c).strip() for c in data.columns]}"
131
+ )
132
+
133
+ scheme_name = data[name_col]
134
+ plan_col = _amfi_resolve_col(colmap, "plan")
135
+ option_col = _amfi_resolve_col(colmap, "option")
136
+ if plan_col is not None or option_col is not None:
137
+ plan = data[plan_col] if plan_col is not None else pd.Series(pd.NA, index=data.index)
138
+ option = data[option_col] if option_col is not None else pd.Series(pd.NA, index=data.index)
139
+ scheme = [_join_amfi_scheme(n, p, o) for n, p, o in zip(scheme_name, plan, option)]
140
+ else:
141
+ scheme = scheme_name
142
+
143
+ repurchase_col = _amfi_resolve_col(colmap, "repurchase price")
144
+ sale_col = _amfi_resolve_col(colmap, "sale price")
145
+ repurchase = data[repurchase_col] if repurchase_col is not None else pd.Series(pd.NA, index=data.index)
146
+ sale = data[sale_col] if sale_col is not None else pd.Series(pd.NA, index=data.index)
147
+ category = data["category"] if "category" in data.columns else pd.Series(pd.NA, index=data.index)
148
+
149
+ return pd.DataFrame(
150
+ {
151
+ "code": data[code_col],
152
+ "scheme": scheme,
153
+ "isin_growth_dividend": data[isin_growth_col],
154
+ "isin_reinvestment": data[isin_reinv_col],
155
+ "nav": data[nav_col],
156
+ "repurchase_price": repurchase,
157
+ "sale_price": sale,
158
+ "date": data[date_col],
159
+ "category": category,
160
+ }
161
+ )
162
+
163
+
164
+ def _amfi_process_category(value):
165
+ try:
166
+ if value == "" or not np.isnan(pd.to_numeric(value, errors="coerce")):
167
+ return np.nan
168
+ except ValueError:
169
+ pass
170
+ return value
171
+
172
+
173
+ def parse_amfi_nav_csv(content: str) -> pd.DataFrame:
174
+ """Parse AMFI NAV history text (legacy or Plan/Option header) into the saved CSV schema."""
175
+ data = pd.read_csv(
176
+ io.StringIO(content),
177
+ sep=";",
178
+ engine="python",
179
+ na_values=["", " "],
180
+ dtype=str,
181
+ skip_blank_lines=False,
182
+ on_bad_lines="warn",
183
+ )
184
+ data.columns = [str(c).strip() for c in data.columns]
185
+ data = data.dropna(how="all")
186
+ rows_to_remove = (data.isna().sum(axis=1) == len(data.columns) - 1) & data.iloc[:, 0].str.contains(
187
+ "Mutual Fund", na=False
188
+ )
189
+ data = data[~rows_to_remove]
190
+ data.loc[:, "category"] = data.iloc[:, 0]
191
+ data["category"] = data["category"].apply(_amfi_process_category)
192
+ data["category"] = data["category"].ffill()
193
+ data = data[data.isna().sum(axis=1) < len(data.columns) - 2]
194
+ data = _canonicalize_amfi_nav_frame(data)
195
+ for col in data.columns:
196
+ data[col] = data[col].apply(lambda x: x.strip() if isinstance(x, str) else x)
197
+ data["date"] = pd.to_datetime(data["date"], format="%d-%b-%Y")
198
+ data["isin_growth_dividend"] = data["isin_growth_dividend"].apply(
199
+ lambda x: x if _is_amfi_isin(x) else pd.NA
200
+ )
201
+ data["isin_reinvestment"] = data["isin_reinvestment"].apply(
202
+ lambda x: x if _is_amfi_isin(x) else pd.NA
203
+ )
204
+ data = data[data["isin_growth_dividend"].notna() | data["isin_reinvestment"].notna()]
205
+ return data[_AMFI_NAV_OUTPUT_COLUMNS]
206
+
207
+
65
208
  def catch(func, *args, handle=lambda e: e, **kwargs):
66
209
  try:
67
210
  return func(*args, **kwargs)
@@ -788,57 +931,7 @@ def save_mf_data(p: str, session, save_to_rds=True):
788
931
  {"url": url, "preview": first_line[:200]},
789
932
  )
790
933
  return None
791
- # AMFI mixes header rows, blank lines, and section titles without delimiters.
792
- # Use the Python engine so pandas can handle rows with variable field counts.
793
- data = pd.read_csv(
794
- io.StringIO(content),
795
- sep=";",
796
- engine="python",
797
- na_values=["", " "],
798
- dtype=str,
799
- skip_blank_lines=False,
800
- )
801
- data = data.dropna(how="all")
802
- rows_to_remove = (data.isna().sum(axis=1) == len(data.columns) - 1) & data.iloc[:, 0].str.contains(
803
- "Mutual Fund", na=False
804
- )
805
- data = data[~rows_to_remove]
806
- # Transpose category to each row
807
- data.loc[:, "category"] = data.iloc[:, 0] # Assuming the category is in the first column
808
- data["category"] = data["category"].apply(process_category)
809
- data["category"] = data["category"].ffill() # Forward-fill the NaN values
810
-
811
- # Delete category headers
812
- # Keep rows where the number of NaNs is less than the total number of columns minus 2
813
- data = data[data.isna().sum(axis=1) < len(data.columns) - 2]
814
- # Rename columns
815
- data.columns = [
816
- "code",
817
- "scheme",
818
- "isin_growth_dividend",
819
- "isin_reinvestment",
820
- "nav",
821
- "repurchase_price",
822
- "sale_price",
823
- "date",
824
- "category",
825
- ]
826
-
827
- # Convert 'date' to datetime format
828
- data["date"] = pd.to_datetime(data["date"], format="%d-%b-%Y")
829
-
830
- # Change 'isin' to NaN if it does not start with 'IN'
831
- data["isin_growth_dividend"] = data["isin_growth_dividend"].apply(
832
- lambda x: x if pd.notna(x) and x.startswith("IN") else pd.NA
833
- )
834
- data["isin_reinvestment"] = data["isin_reinvestment"].apply(
835
- lambda x: x if pd.notna(x) and x.startswith("IN") else pd.NA
836
- )
837
-
838
- # Filter rows that have 'isin' to exclude unclaimed dividend info
839
- data = data[data["isin_growth_dividend"].notna() | data["isin_reinvestment"].notna()]
840
-
841
- # Save DataFrame to CSV
934
+ data = parse_amfi_nav_csv(content)
842
935
  save_pandas_in_csv_out(data, dest_file, index=True)
843
936
 
844
937
  get_bootlick_logger().log_info(
@@ -860,16 +953,6 @@ def save_mf_data(p: str, session, save_to_rds=True):
860
953
  return None
861
954
  return dest_file
862
955
 
863
- # Custom function to handle the logic
864
- def process_category(value):
865
- try:
866
- # Check if the value can be coerced into a numeric type or is an empty string
867
- if value == "" or not np.isnan(pd.to_numeric(value, errors="coerce")):
868
- return np.nan # Replace with NaN
869
- except ValueError:
870
- pass
871
- return value # Keep the original value if it cannot be coerced
872
-
873
956
  try:
874
957
  outfolder = get_dynamic_config().get("daily_prices")
875
958
  if outfolder is None:
@@ -907,10 +990,12 @@ def save_mf_data(p: str, session, save_to_rds=True):
907
990
  f"Processing date: {p}\n\n"
908
991
  f"Please ensure the CSV file is downloaded first using download mode before processing.",
909
992
  )
910
- return
993
+ return False
911
994
  else:
912
995
  # In download mode, download the file
913
996
  dest_file = download_csv(url, dest_file)
997
+ if dest_file is None or not file_exists_and_valid(dest_file, min_size=1000):
998
+ return False
914
999
  if not save_to_rds or dest_file is None or not file_exists_and_valid(dest_file, min_size=1000):
915
1000
  return
916
1001
  if file_exists_and_valid(dest_file):
@@ -1021,6 +1106,7 @@ def save_mf_data(p: str, session, save_to_rds=True):
1021
1106
  f"Unable to process mutual fund data for {p}",
1022
1107
  f"{traceback.format_exc()}",
1023
1108
  )
1109
+ return False
1024
1110
 
1025
1111
 
1026
1112
  def save_option_data_old(p: str, session, save_to_rds: bool = True):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bootlick
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A package for specific websites
5
5
  Author-email: Unknown <unknown@example.com>
6
6
  License-Expression: MIT
@@ -27,7 +27,7 @@ include-package-data = true
27
27
 
28
28
  [project]
29
29
  name = "bootlick"
30
- version = "0.2.2"
30
+ version = "0.2.3"
31
31
  description = "A package for specific websites"
32
32
  readme = "README.md"
33
33
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes