Skip to content

Commit

Permalink
- Correct gh create command in python publish GH workflow
Browse files Browse the repository at this point in the history
- Fix: BSE no longer exits on error.
- Windows Fix: Path.rename raises FileExistsError for existing files
- raise RuntimeError in BSE.__download if file unavailable
- raise TimeoutError in BSE.__download if request timeout
- Skip file size checks in BSE.deliveryReport and BSE.bhavcopyReport
- Updated documentation
  • Loading branch information
BennyThadikaran committed Nov 3, 2023
1 parent d6d1b4a commit a7f3ebd
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 24 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@ jobs:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Create Github release
- name: Create GitHub release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release upload
'${{ github.ref_name }}' dist/**
gh release create
'${{ github.ref_name }}'
--repo '${{ github.repository }}'
--notes ""
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
Expand Down
78 changes: 57 additions & 21 deletions src/bse/BSE.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@


class BSE:
'''Unofficial Python Api for BSE India'''
'''Unofficial Python Api for BSE India
:param download_folder: A folder/dir to save downloaded files and cookie files
:type download_folder: pathlib.Path or str
:raise ValueError: if ``download_folder`` is not a folder/dir
'''

base_url = 'https://www.bseindia.com/'
api_url = 'https://api.bseindia.com/BseIndiaAPI/api'
Expand All @@ -30,7 +35,7 @@ class BSE:
'MT', 'P', 'R', 'T', 'TS', 'W', 'X', 'XD', 'XT', 'Y', 'Z',
'ZP', 'ZY')

def __init__(self):
def __init__(self, download_folder: str | Path):
self.session = Session()
ua = 'Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/118.0'

Expand All @@ -42,16 +47,15 @@ def __init__(self):
'Referer': self.base_url,
})

self.dir = BSE.__getPath(download_folder, isFolder=True)

def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, exc_traceback):
def __exit__(self, *_):
self.session.close()

if exc_type:
exit(f'{exc_type}: {exc_value} | {exc_traceback}')

return True
return False

def exit(self):
'''Close the Request session'''
Expand All @@ -78,13 +82,17 @@ def __download(self, url: str, folder: Path):
try:
with self.session.get(url,
stream=True,
timeout=15) as r:
timeout=10) as r:

if r.status_code == 404:
raise RuntimeError(
'Report is unavailable or not yet updated.')

with fname.open(mode='wb') as f:
for chunk in r.iter_content(chunk_size=1000000):
f.write(chunk)
except Exception as e:
exit(f'Download error. Try again later: {e!r}')
except ReadTimeout:
raise TimeoutError('Request timed out')

return fname

Expand Down Expand Up @@ -130,65 +138,69 @@ def __getPath(path: str | Path, isFolder: bool = False):

return path

def bhavcopyReport(self, date: datetime, folder: str | Path):
def bhavcopyReport(self, date: datetime, folder: str | Path | None = None):
'''
Download the daily bhavcopy report for specified ``date``
:param date: date of report
:type date: datetime.datetime
:param folder: dir/folder to download the file to
:type folder: str or pathlib.Path
:param folder: Optional dir/folder to save the file to
:type folder: str or pathlib.Path or None
:raise ValueError: if ``folder`` is not a dir/folder.
:raise RuntimeError: if report is unavailable or not yet updated.
:raise FileNotFoundError: if file download failed or file is corrupt.
:raise TimeoutError: if request timed out with no response
:return: file path of downloaded report
:rtype: pathlib.Path
Zip file is extracted and saved filepath returned.
'''

folder = BSE.__getPath(folder, isFolder=True)
folder = BSE.__getPath(folder, isFolder=True) if folder else self.dir

url = f'{self.base_url}/download/BhavCopy/Equity/EQ_ISINCODE_{date:%d%m%y}.zip'

file = self.__download(url, folder)

if not file.is_file() or file.stat().st_size < 5000:
if not file.exists():
file.unlink()
raise FileNotFoundError(f'Failed to download file: {file.name}')

return BSE.__unzip(file, file.parent)

def deliveryReport(self, date: datetime, folder: str | Path):
def deliveryReport(self, date: datetime, folder: str | Path | None = None):
'''
Download the daily delivery report for specified ``date``
:param date: date of report
:type date: datetime.datetime
:param folder: dir/folder to download the file to
:type folder: str or pathlib.Path
:param folder: Optional dir/folder to save the file to
:type folder: str or pathlib.Path or None
:raise ValueError: if ``folder`` is not a dir/folder.
:raise RuntimeError: if report is unavailable or not yet updated.
:raise FileNotFoundError: if file download failed or file is corrupt.
:raise TimeoutError: if request timed out with no response
:return: file path of downloaded report
:rtype: pathlib.Path
Zip file is extracted, converted to CSV, and saved filepath is returned
'''

folder = BSE.__getPath(folder, isFolder=True)
folder = BSE.__getPath(folder, isFolder=True) if folder else self.dir

url = f'{self.base_url}/BSEDATA/gross/{date:%Y}/SCBSEALL{date:%d%m}.zip'

file = self.__download(url, folder)

if not file.is_file() or file.stat().st_size < 5000:
if not file.exists():
file.unlink()
raise FileNotFoundError(f'Failed to download file: {file.name}')

file = BSE.__unzip(file, file.parent)

file.write_bytes(file.read_bytes().replace(b'|', b','))

return file.rename(file.with_suffix('.csv'))
return file.replace(file.with_suffix('.csv'))

def announcements(self,
page_no: int = 1,
Expand All @@ -212,6 +224,8 @@ def announcements(self,
:param subcategory: (Optional). Filter announcements by subcategory ex. ``Dividend``.
:type subcategory: str
:raise ValueError: if ``from_date`` is greater than ``to_date`` or ``subcategory`` argument is passed without ``category``
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: All announcements. `Sample response <https://github.com/BennyThadikaran/BseIndiaApi/blob/main/src/samples/announcements.json>`__
:rtype: dict[str, list[dict]]
Expand Down Expand Up @@ -308,6 +322,8 @@ def actions(self,
:param purpose_code: Limit result to actions with given purpose
:type purpose_code: str
:raise ValueError: if ``from_date`` is greater than ``to_date``
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: List of actions. `Sample response <https://github.com/BennyThadikaran/BseIndiaApi/blob/main/src/samples/actions.json>`__
:rtype: list[dict]
Expand Down Expand Up @@ -376,6 +392,8 @@ def resultCalendar(self,
:param scripcode: (Optional). Limit result to stock symbol
:type scripcode: str
:raise ValueError: if ``from_date`` is greater than ``to_date``
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: List of Corporate results. `Sample response <https://github.com/BennyThadikaran/BseIndiaApi/blob/main/src/samples/resultCalendar.json>`__
:rtype: list[dict]
Expand Down Expand Up @@ -411,6 +429,8 @@ def advanceDecline(self) -> list[dict]:
'''
Advance decline values for all BSE indices
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: Advance decline values. `Sample response <https://github.com/BennyThadikaran/BseIndiaApi/blob/main/src/samples/advanceDecline.json>`__
:rtype: list[dict]
'''
Expand All @@ -437,6 +457,8 @@ def gainers(self,
:param pct_change: Default ``all``. Filter stocks by percent change. One of ``10``, ``5``, ``2``, ``0``.
:type pct_change: str
:raise ValueError: if ``name`` is not a valid BSE stock group.
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: List of top gainers by percent change. `Sample response <https://github.com/BennyThadikaran/BseIndiaApi/blob/main/src/samples/gainers.json>`__
:rtype: list[dict]
Expand Down Expand Up @@ -500,6 +522,8 @@ def losers(self,
:param pct_change: Default ``all``. Filter stocks by percent change. One of ``10``, ``5``, ``2``, ``0``.
:type pct_change: str
:raise ValueError: if ``name`` is not a valid BSE stock group.
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: List of top losers by percent change. `Sample response <https://github.com/BennyThadikaran/BseIndiaApi/blob/main/src/samples/losers.json>`__
:rtype: list[dict]
Expand Down Expand Up @@ -562,6 +586,8 @@ def near52WeekHighLow(self,
:param name: (Optional). Stock group name or Market index name.
:type name: str
:raise ValueError: if ``name`` is not a valid BSE stock group.
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: Stocks near 52 week high and lows. `Sample response <https://github.com/BennyThadikaran/BseIndiaApi/blob/main/src/samples/near52WeekHighLow.json>`__
:rtype: dict
Expand Down Expand Up @@ -625,6 +651,8 @@ def quote(self, scripcode) -> dict[str, float]:
:param scripcode: BSE scrip code
:type scripcode: str
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: OHLC data for given scripcode. `Sample response <https://github.com/BennyThadikaran/BseIndiaApi/blob/main/src/samples/quote.json>`__
:rtype: dict[str, float]
'''
Expand Down Expand Up @@ -654,6 +682,8 @@ def quoteWeeklyHL(self, scripcode) -> dict:
:param scripcode: BSE scrip code
:type scripcode: str
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: Weekly and monthly high and lows with dates. `Sample response <https://github.com/BennyThadikaran/BseIndiaApi/blob/main/src/samples/quoteWeeklyHL.json>`__
:rtype: dict
'''
Expand Down Expand Up @@ -700,6 +730,8 @@ def listSecurities(self,
:param segment: Default 'Equity'. One of ``equity``, ``mf``, ``Preference Shares``, ``Debentures and Bonds``, ``Equity - Institutional Series``, ``Commercial Papers``
:param status: Default 'Active'. One of ``active``, ``suspended``, or ``delisted``
:raise ValueError: if ``group`` is not a valid BSE stock group
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: list of securities with meta info. `Sample response <https://github.com/BennyThadikaran/BseIndiaApi/blob/main/src/samples/listSecurities.json>`__
:rtype: list[dict]
Expand Down Expand Up @@ -744,6 +776,8 @@ def getScripName(self, scripcode) -> str:
:param scripcode: BSE scrip code
:raise ValueError: if scrip not found
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: Symbol code
:rtype: str
Expand All @@ -768,6 +802,8 @@ def getScripCode(self, scripname):
:param scripname: Stock symbol code
:raise ValueError: if scrip not found
:raise TimeoutError: if request timed out with no response
:raise ConnectionError: in case of HTTP error or server returns error response.
:return: BSE scrip code
:rtype: str
Expand Down

0 comments on commit a7f3ebd

Please sign in to comment.