Subham9126 commited on
Commit
f6ca824
·
verified ·
1 Parent(s): d0bfe46

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +70 -164
app.py CHANGED
@@ -2,13 +2,12 @@ import gradio as gr
2
  import re
3
  import asyncio
4
  import aiohttp
5
- from datetime import datetime, time
6
  from typing import Dict, Optional, List
7
  import pytz
8
  import tzlocal
9
  import logging
10
  import json
11
- from datetime import date
12
 
13
  # --- Basic Setup ---
14
 
@@ -21,36 +20,23 @@ class DateTimeValidationError(ValueError):
21
  """Custom exception for datetime validation errors."""
22
  pass
23
 
24
- # 3. Assume hist_url is globally defined
25
  hist_url = "https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE/segment/CASH"
26
 
27
 
28
  # --- Date/Time Functions ---
29
 
30
  def validate_datetime_format(dt_str: str) -> datetime:
31
- """
32
- Validate datetime string in strict 'YYYY-MM-DD' format.
33
- """
34
  if not isinstance(dt_str, str):
35
- logger.error(f"Expected string input, got {type(dt_str).__name__}")
36
  raise TypeError(f"Input must be a string, got {type(dt_str).__name__}")
37
-
38
  date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}$')
39
  if not date_pattern.match(dt_str):
40
- logger.error(f"Invalid date format: '{dt_str}'. Expected 'YYYY-MM-DD'")
41
- raise DateTimeValidationError(
42
- f"Invalid date format: '{dt_str}'. Expected 'YYYY-MM-DD'"
43
- )
44
-
45
  try:
46
- parsed_date = datetime.strptime(dt_str, '%Y-%m-%d')
47
- logger.debug(f"Successfully validated date: {dt_str}")
48
- return parsed_date
49
  except ValueError as e:
50
- logger.error(f"Invalid date value: '{dt_str}' - {str(e)}")
51
- raise DateTimeValidationError(
52
- f"Invalid date value: '{dt_str}'. Please provide a valid calendar date."
53
- ) from e
54
 
55
  def _resolve_timezone(timezone: Optional[str]) -> pytz.BaseTzInfo:
56
  """Resolve timezone string to pytz timezone object."""
@@ -58,226 +44,146 @@ def _resolve_timezone(timezone: Optional[str]) -> pytz.BaseTzInfo:
58
  try:
59
  return pytz.timezone(timezone)
60
  except pytz.UnknownTimeZoneError as e:
61
- logger.error(f"Unknown timezone: '{timezone}'")
62
  raise ValueError(f"Unknown timezone: '{timezone}'") from e
63
- else:
64
- local_tz = tzlocal.get_localzone()
65
- logger.debug(f"Using local timezone: {local_tz}")
66
- return local_tz
67
 
68
- def convert_to_unixtimestamp(
69
- date_time_str: str,
70
- timezone: Optional[str] = None
71
- ) -> int:
72
- """
73
- Convert datetime string to Unix timestamp in milliseconds with timezone handling.
74
- """
75
  if not isinstance(date_time_str, str):
76
- logger.error(f"Expected string input, got {type(date_time_str).__name__}")
77
- raise TypeError(
78
- f"DateTime input must be a string in 'YYYY-MM-DD HH:MM' format, "
79
- f"got {type(date_time_str).__name__}"
80
- )
81
-
82
  try:
83
  dt = datetime.strptime(date_time_str, '%Y-%m-%d %H:%M')
84
- logger.debug(f"Successfully parsed datetime: {date_time_str}")
85
  except ValueError as e:
86
- logger.error(f"Invalid datetime format: '{date_time_str}'")
87
- raise DateTimeValidationError(
88
- f"Invalid datetime format: '{date_time_str}'. Expected 'YYYY-MM-DD HH:MM'"
89
- ) from e
90
-
91
  target_tz = _resolve_timezone(timezone)
 
 
92
 
93
- if dt.tzinfo is None:
94
- localized_dt = target_tz.localize(dt)
95
- else:
96
- localized_dt = dt.astimezone(target_tz)
97
-
98
- timestamp_ms = int(localized_dt.timestamp() * 1000)
99
- logger.debug(f"Converted '{date_time_str}' to timestamp: {timestamp_ms}")
100
-
101
- return timestamp_ms
102
-
103
- def get_time_range_in_unix_ms(
104
- start_date_str: str,
105
- end_date_str: str,
106
- timezone: str = 'Asia/Kolkata'
107
- ) -> Dict[str, int]:
108
- """
109
- Calculates the start and end Unix timestamps in milliseconds for a date range.
110
- """
111
  start_date = validate_datetime_format(start_date_str)
112
  end_date = validate_datetime_format(end_date_str)
113
-
114
  start_datetime = datetime.combine(start_date, time.min)
115
  end_datetime = datetime.combine(end_date, time(23, 59))
116
-
117
- start_datetime_str = start_datetime.strftime('%Y-%m-%d %H:%M')
118
- end_datetime_str = end_datetime.strftime('%Y-%m-%d %H:%M')
119
-
120
- start_timestamp = convert_to_unixtimestamp(start_datetime_str, timezone)
121
- end_timestamp = convert_to_unixtimestamp(end_datetime_str, timezone)
122
-
123
- return {
124
- "start_timestamp_ms": start_timestamp,
125
- "end_timestamp_ms": end_timestamp,
126
- }
127
 
128
 
129
  # --- Asynchronous API Function ---
130
 
131
- async def call_price_api_async(
132
- session: aiohttp.ClientSession,
133
- ticker: str,
134
- start: int,
135
- end: int,
136
- interval: int,
137
- ) -> Dict:
138
- """
139
- Asynchronously calls the Groww candle API and returns the raw JSON response.
140
- """
141
  url = f"{hist_url}/{ticker}"
142
- params = {
143
- "startTimeInMillis": start,
144
- "endTimeInMillis": end,
145
- "intervalInMinutes": interval,
146
- }
147
-
148
  try:
149
  async with session.get(url, params=params) as response:
150
  response.raise_for_status()
151
  json_data = await response.json()
152
- return {
153
- "ticker": ticker,
154
- "interval": interval,
155
- "data": json_data,
156
- "error": None,
157
- }
158
  except aiohttp.ClientError as e:
159
- return {
160
- "ticker": ticker,
161
- "interval": interval,
162
- "data": None,
163
- "error": str(e),
164
- }
165
 
166
 
167
  # --- Main Processing Logic ---
168
 
169
- async def main(tickers: List[str], start_time: int, end_time: int, intervals: List[int], progress: gr.Progress):
170
- """
171
- Main function to run the asynchronous API calls in batches.
172
- """
173
  results = []
174
  batch_size = 30
175
-
176
  async with aiohttp.ClientSession() as session:
177
  for i in progress.tqdm(range(0, len(tickers), batch_size), desc="Processing Batches"):
178
  batch_tickers = tickers[i:i+batch_size]
179
- tasks = []
180
- for ticker in batch_tickers:
181
- for interval in intervals:
182
- tasks.append(
183
- call_price_api_async(session, ticker, start_time, end_time, interval)
184
- )
185
-
186
  batch_results = await asyncio.gather(*tasks)
187
  results.extend(batch_results)
188
-
189
  return results
190
 
191
  def process_and_merge_data(results: list) -> str:
192
- """
193
- Processes and merges the raw API results for different intervals.
194
- """
195
  merged_data = {}
196
-
197
  for result in results:
198
- ticker = result.get("ticker")
199
- interval = result.get("interval")
200
- data = result.get("data")
201
- error = result.get("error")
202
-
203
  if ticker not in merged_data:
204
  merged_data[ticker] = {"symbol": ticker}
205
-
206
  if error:
207
  merged_data[ticker][f"error_{interval}m"] = error
208
  continue
209
-
210
  if data and data.get("candles") and data["candles"]:
211
  first_candle = data["candles"][0]
212
-
213
- if interval == 1440:
214
- merged_data[ticker]["day open"] = first_candle[1] if len(first_candle) > 1 else None
215
- merged_data[ticker]["day high"] = first_candle[2] if len(first_candle) > 2 else None
216
- merged_data[ticker]["day low"] = first_candle[3] if len(first_candle) > 3 else None
217
- merged_data[ticker]["day close"] = first_candle[4] if len(first_candle) > 4 else None
218
- elif interval == 15:
219
- merged_data[ticker]["start open"] = first_candle[1] if len(first_candle) > 1 else None
220
- merged_data[ticker]["start high"] = first_candle[2] if len(first_candle) > 2 else None
221
- merged_data[ticker]["start low"] = first_candle[3] if len(first_candle) > 3 else None
222
- merged_data[ticker]["start close"] = first_candle[4] if len(first_candle) > 4 else None
223
  else:
224
  merged_data[ticker][f"error_{interval}m"] = "No data or candles found"
 
 
225
 
226
- final_results = list(merged_data.values())
227
- return json.dumps(final_results, indent=4)
228
-
229
- # --- Gradio Interface ---
230
 
231
  async def run_backend_processing(tickers_text: str, progress=gr.Progress(track_tqdm=True)):
232
- """
233
- The main function to be called by the Gradio interface.
234
- """
235
  if not tickers_text.strip():
236
- return "Please enter at least one ticker.", "{}"
 
237
 
238
- tickers = [ticker.strip() for ticker in tickers_text.split('\n') if ticker.strip()]
239
-
240
  today_str = date.today().strftime("%Y-%m-%d")
241
-
242
  yield f"Starting processing for {len(tickers)} tickers for date: {today_str}", "{}"
243
 
244
  try:
245
  time_range = get_time_range_in_unix_ms(today_str, today_str)
246
- start_time = time_range["start_timestamp_ms"]
247
- end_time = time_range["end_timestamp_ms"]
248
  intervals = [1440, 15]
249
 
250
- yield "Fetching data from API...", "{}"
251
-
252
- results = await main(tickers, start_time, end_time, intervals, progress)
253
 
254
  yield "Processing and merging data...", "{}"
255
-
256
  processed_json = process_and_merge_data(results)
257
 
258
  yield "Processing complete.", processed_json
259
 
260
  except Exception as e:
261
- logger.error(f"An error occurred: {e}")
262
  yield f"An error occurred: {e}", "{}"
263
 
264
- with gr.Blocks() as demo:
265
- gr.Markdown("# Backend Processing Logs")
 
 
 
266
 
267
  with gr.Row():
268
  with gr.Column(scale=1):
269
- tickers_input = gr.Textbox(lines=10, label="Enter Tickers (one per line)")
270
- start_button = gr.Button("Start Processing")
 
 
 
 
 
271
 
272
  with gr.Column(scale=2):
273
  logs_output = gr.Textbox(label="Logs", lines=15, interactive=False)
274
  json_output = gr.JSON(label="Processed JSON Output")
275
 
276
- start_button.click(
277
- fn=run_backend_processing,
278
- inputs=[tickers_input],
279
- outputs=[logs_output, json_output]
280
- )
 
 
 
 
 
 
281
 
282
  if __name__ == "__main__":
283
  demo.launch()
 
2
  import re
3
  import asyncio
4
  import aiohttp
5
+ from datetime import datetime, time, date
6
  from typing import Dict, Optional, List
7
  import pytz
8
  import tzlocal
9
  import logging
10
  import json
 
11
 
12
  # --- Basic Setup ---
13
 
 
20
  """Custom exception for datetime validation errors."""
21
  pass
22
 
23
+ # 3. API URL
24
  hist_url = "https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE/segment/CASH"
25
 
26
 
27
  # --- Date/Time Functions ---
28
 
29
  def validate_datetime_format(dt_str: str) -> datetime:
30
+ """Validate datetime string in strict 'YYYY-MM-DD' format."""
 
 
31
  if not isinstance(dt_str, str):
 
32
  raise TypeError(f"Input must be a string, got {type(dt_str).__name__}")
 
33
  date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}$')
34
  if not date_pattern.match(dt_str):
35
+ raise DateTimeValidationError(f"Invalid date format: '{dt_str}'. Expected 'YYYY-MM-DD'")
 
 
 
 
36
  try:
37
+ return datetime.strptime(dt_str, '%Y-%m-%d')
 
 
38
  except ValueError as e:
39
+ raise DateTimeValidationError(f"Invalid date value: '{dt_str}'.") from e
 
 
 
40
 
41
  def _resolve_timezone(timezone: Optional[str]) -> pytz.BaseTzInfo:
42
  """Resolve timezone string to pytz timezone object."""
 
44
  try:
45
  return pytz.timezone(timezone)
46
  except pytz.UnknownTimeZoneError as e:
 
47
  raise ValueError(f"Unknown timezone: '{timezone}'") from e
48
+ return tzlocal.get_localzone()
 
 
 
49
 
50
+ def convert_to_unixtimestamp(date_time_str: str, timezone: Optional[str] = None) -> int:
51
+ """Convert 'YYYY-MM-DD HH:MM' string to Unix timestamp in milliseconds."""
 
 
 
 
 
52
  if not isinstance(date_time_str, str):
53
+ raise TypeError(f"DateTime input must be a string, got {type(date_time_str).__name__}")
 
 
 
 
 
54
  try:
55
  dt = datetime.strptime(date_time_str, '%Y-%m-%d %H:%M')
 
56
  except ValueError as e:
57
+ raise DateTimeValidationError(f"Invalid format: '{date_time_str}'. Expected 'YYYY-MM-DD HH:MM'") from e
58
+
 
 
 
59
  target_tz = _resolve_timezone(timezone)
60
+ localized_dt = target_tz.localize(dt) if dt.tzinfo is None else dt.astimezone(target_tz)
61
+ return int(localized_dt.timestamp() * 1000)
62
 
63
+ def get_time_range_in_unix_ms(start_date_str: str, end_date_str: str, timezone: str = 'Asia/Kolkata') -> Dict[str, int]:
64
+ """Calculates the start (00:00) and end (23:59) Unix timestamps for a date range."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  start_date = validate_datetime_format(start_date_str)
66
  end_date = validate_datetime_format(end_date_str)
 
67
  start_datetime = datetime.combine(start_date, time.min)
68
  end_datetime = datetime.combine(end_date, time(23, 59))
69
+ start_timestamp = convert_to_unixtimestamp(start_datetime.strftime('%Y-%m-%d %H:%M'), timezone)
70
+ end_timestamp = convert_to_unixtimestamp(end_datetime.strftime('%Y-%m-%d %H:%M'), timezone)
71
+ return {"start_timestamp_ms": start_timestamp, "end_timestamp_ms": end_timestamp}
 
 
 
 
 
 
 
 
72
 
73
 
74
  # --- Asynchronous API Function ---
75
 
76
+ async def call_price_api_async(session: aiohttp.ClientSession, ticker: str, start: int, end: int, interval: int) -> Dict:
77
+ """Asynchronously calls the candle API."""
 
 
 
 
 
 
 
 
78
  url = f"{hist_url}/{ticker}"
79
+ params = {"startTimeInMillis": start, "endTimeInMillis": end, "intervalInMinutes": interval}
 
 
 
 
 
80
  try:
81
  async with session.get(url, params=params) as response:
82
  response.raise_for_status()
83
  json_data = await response.json()
84
+ return {"ticker": ticker, "interval": interval, "data": json_data, "error": None}
 
 
 
 
 
85
  except aiohttp.ClientError as e:
86
+ return {"ticker": ticker, "interval": interval, "data": None, "error": str(e)}
 
 
 
 
 
87
 
88
 
89
  # --- Main Processing Logic ---
90
 
91
+ async def main_task(tickers: List[str], start_time: int, end_time: int, intervals: List[int], progress: gr.Progress):
92
+ """Main function to run the asynchronous API calls in batches."""
 
 
93
  results = []
94
  batch_size = 30
 
95
  async with aiohttp.ClientSession() as session:
96
  for i in progress.tqdm(range(0, len(tickers), batch_size), desc="Processing Batches"):
97
  batch_tickers = tickers[i:i+batch_size]
98
+ tasks = [call_price_api_async(session, ticker, start_time, end_time, interval)
99
+ for ticker in batch_tickers for interval in intervals]
 
 
 
 
 
100
  batch_results = await asyncio.gather(*tasks)
101
  results.extend(batch_results)
 
102
  return results
103
 
104
  def process_and_merge_data(results: list) -> str:
105
+ """Processes and merges the raw API results for different intervals."""
 
 
106
  merged_data = {}
 
107
  for result in results:
108
+ ticker, interval, data, error = result.get("ticker"), result.get("interval"), result.get("data"), result.get("error")
 
 
 
 
109
  if ticker not in merged_data:
110
  merged_data[ticker] = {"symbol": ticker}
 
111
  if error:
112
  merged_data[ticker][f"error_{interval}m"] = error
113
  continue
 
114
  if data and data.get("candles") and data["candles"]:
115
  first_candle = data["candles"][0]
116
+ if len(first_candle) > 4:
117
+ prefix = "day" if interval == 1440 else "start"
118
+ merged_data[ticker][f"{prefix} open"] = first_candle[1]
119
+ merged_data[ticker][f"{prefix} high"] = first_candle[2]
120
+ merged_data[ticker][f"{prefix} low"] = first_candle[3]
121
+ merged_data[ticker][f"{prefix} close"] = first_candle[4]
 
 
 
 
 
122
  else:
123
  merged_data[ticker][f"error_{interval}m"] = "No data or candles found"
124
+
125
+ return json.dumps(list(merged_data.values()), indent=4)
126
 
127
+ # --- Gradio Backend Function ---
 
 
 
128
 
129
  async def run_backend_processing(tickers_text: str, progress=gr.Progress(track_tqdm=True)):
130
+ """The main function to be called by the Gradio interface."""
 
 
131
  if not tickers_text.strip():
132
+ yield "Please enter at least one ticker.", "{}"
133
+ return # Correct way to exit an async generator
134
 
135
+ tickers = [ticker.strip().upper() for ticker in tickers_text.split('\n') if ticker.strip()]
 
136
  today_str = date.today().strftime("%Y-%m-%d")
 
137
  yield f"Starting processing for {len(tickers)} tickers for date: {today_str}", "{}"
138
 
139
  try:
140
  time_range = get_time_range_in_unix_ms(today_str, today_str)
141
+ start_time, end_time = time_range["start_timestamp_ms"], time_range["end_timestamp_ms"]
 
142
  intervals = [1440, 15]
143
 
144
+ yield "Fetching data from API in batches...", "{}"
145
+ results = await main_task(tickers, start_time, end_time, intervals, progress)
 
146
 
147
  yield "Processing and merging data...", "{}"
 
148
  processed_json = process_and_merge_data(results)
149
 
150
  yield "Processing complete.", processed_json
151
 
152
  except Exception as e:
153
+ logger.error(f"An unhandled error occurred: {e}")
154
  yield f"An error occurred: {e}", "{}"
155
 
156
+ # --- Gradio UI ---
157
+
158
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
159
+ gr.Markdown("## Stock Data Backend Processor")
160
+ gr.Markdown("Enter stock tickers (one per line) and click 'Start Processing' or press Enter.")
161
 
162
  with gr.Row():
163
  with gr.Column(scale=1):
164
+ # Pre-fill the textbox with the example tickers
165
+ tickers_input = gr.Textbox(
166
+ lines=10,
167
+ label="Enter Tickers",
168
+ value="RELIANCE\nINFY"
169
+ )
170
+ start_button = gr.Button("Start Processing", variant="primary")
171
 
172
  with gr.Column(scale=2):
173
  logs_output = gr.Textbox(label="Logs", lines=15, interactive=False)
174
  json_output = gr.JSON(label="Processed JSON Output")
175
 
176
+ # --- Triggers ---
177
+
178
+ # Define a list of components that trigger the function
179
+ triggers = [start_button.click, tickers_input.submit]
180
+
181
+ for event in triggers:
182
+ event(
183
+ fn=run_backend_processing,
184
+ inputs=[tickers_input],
185
+ outputs=[logs_output, json_output]
186
+ )
187
 
188
  if __name__ == "__main__":
189
  demo.launch()