diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..af690fc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python-envs.pythonProjects": [] +} diff --git a/stock-scraper/.gitignore b/stock-scraper/.gitignore new file mode 100644 index 0000000..ee6c65a --- /dev/null +++ b/stock-scraper/.gitignore @@ -0,0 +1,21 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Ruff +**.ruff_cache/ + +# Virtual environments +.venv + +# Vagrant +.vagrant + +# user data directory +_userdatadir/ +_userdatachrome/ +_userdatafirefox/ diff --git a/stock-scraper/.python-version b/stock-scraper/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/stock-scraper/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/stock-scraper/Makefile b/stock-scraper/Makefile new file mode 100755 index 0000000..3ba2436 --- /dev/null +++ b/stock-scraper/Makefile @@ -0,0 +1,27 @@ +# note: call scripts from /scripts +SHELL := /bin/bash # use this instead of /bin/bash + +PROG_ROOT := webapp +PROG_NAME := main.py + +.PHONY: all sync format purge +all: sync format-check + +# cleans up everything including database and venv +purge: cleandb + -rm -rf .venv + -rm -rf .tox + -rm -rf __pycache__ + +# format the code +format: +# uv tool run ruff format . + uv format -- --line-length 120 + +format-check: + uv format --diff + uv format --check + +# sync the dependencies including venv +sync: + uv sync diff --git a/stock-scraper/README.md b/stock-scraper/README.md new file mode 100644 index 0000000..92f4876 --- /dev/null +++ b/stock-scraper/README.md @@ -0,0 +1,21 @@ +# Stock Scraper + + +### Setup + +```sh +uv format # format python code +uv sync # sync the repositories + +# copy chrome configs, see chrome://version in the browser +cp -r ~/.config/google-chrome/ _userdatachrome + +# copy firefox configs, see about:profiles + +uv run main.py + +``` + +### Notes + +The first time the script is run, you have to manually authenticate yourself. diff --git a/stock-scraper/a.py b/stock-scraper/a.py new file mode 100644 index 0000000..c56be29 --- /dev/null +++ b/stock-scraper/a.py @@ -0,0 +1,69 @@ +# Scrapes the tickers from the website +# import libraries +from selenium import webdriver +from selenium.webdriver.common.by import By +import time, os, csv, sys +from selenium.webdriver.common.action_chains import ActionChains + +def main(): + # Set options (prevents the browser from closing after opening) + options = webdriver.ChromeOptions() + options.add_experimental_option("detach", True) + # options.add_argument('--headless') + options.add_argument('--no-sandbox') + # Open the browser + print('Opening browser') + driver = webdriver.Chrome(options=options) + print('Opened browser') + actions = ActionChains(driver) + # Maximize the browser to fullscreen + driver.maximize_window() + print('Maxed browser') + # Open the link + driver.implicitly_wait(10) + driver.get('https://screener.musaffa.com/cabinet/onboarding') + page_number = 1 + for i in range(759): + # gather company names + company_names = driver.find_element(By.CLASS_NAME, 'table--body').find_elements(By.CLASS_NAME, "mb-0.company--name") + company_names = list(map(lambda name: name.text, company_names)) + + # gather company tickers + company_tickers = driver.find_element(By.CLASS_NAME, 'table--body').find_elements(By.CLASS_NAME, "mb-0.stock--name") + company_tickers = list(map(lambda name: name.text, company_tickers)) + + # save the links + save(company_names, company_tickers, page_number) + # go to the next page + if page_number != 3: + go_to_next_page(driver) + page_number += 1 + + +def go_to_next_page(driver): + driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") + time.sleep(3.5) + driver.find_element(By.CLASS_NAME, 'bi.bi-chevron-right').click() + + + + +def save(company_names, company_tickers, page_number): + i = 0 + with open('musaffa_tickers.csv', 'a') as file: + writer = csv.DictWriter(file, fieldnames=["company_name", "company_ticker", "page_number"]) + if page_number == 1: + writer.writeheader() + for company_name in company_names: + writer.writerow({"company_name": company_name, + "company_ticker": company_tickers[i], + "page_number": page_number}) + i += 1 + print(f"Saved page {page_number}") + + + + + +if __name__ == "__main__": + main() diff --git a/stock-scraper/b.py b/stock-scraper/b.py new file mode 100644 index 0000000..c56be29 --- /dev/null +++ b/stock-scraper/b.py @@ -0,0 +1,69 @@ +# Scrapes the tickers from the website +# import libraries +from selenium import webdriver +from selenium.webdriver.common.by import By +import time, os, csv, sys +from selenium.webdriver.common.action_chains import ActionChains + +def main(): + # Set options (prevents the browser from closing after opening) + options = webdriver.ChromeOptions() + options.add_experimental_option("detach", True) + # options.add_argument('--headless') + options.add_argument('--no-sandbox') + # Open the browser + print('Opening browser') + driver = webdriver.Chrome(options=options) + print('Opened browser') + actions = ActionChains(driver) + # Maximize the browser to fullscreen + driver.maximize_window() + print('Maxed browser') + # Open the link + driver.implicitly_wait(10) + driver.get('https://screener.musaffa.com/cabinet/onboarding') + page_number = 1 + for i in range(759): + # gather company names + company_names = driver.find_element(By.CLASS_NAME, 'table--body').find_elements(By.CLASS_NAME, "mb-0.company--name") + company_names = list(map(lambda name: name.text, company_names)) + + # gather company tickers + company_tickers = driver.find_element(By.CLASS_NAME, 'table--body').find_elements(By.CLASS_NAME, "mb-0.stock--name") + company_tickers = list(map(lambda name: name.text, company_tickers)) + + # save the links + save(company_names, company_tickers, page_number) + # go to the next page + if page_number != 3: + go_to_next_page(driver) + page_number += 1 + + +def go_to_next_page(driver): + driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") + time.sleep(3.5) + driver.find_element(By.CLASS_NAME, 'bi.bi-chevron-right').click() + + + + +def save(company_names, company_tickers, page_number): + i = 0 + with open('musaffa_tickers.csv', 'a') as file: + writer = csv.DictWriter(file, fieldnames=["company_name", "company_ticker", "page_number"]) + if page_number == 1: + writer.writeheader() + for company_name in company_names: + writer.writerow({"company_name": company_name, + "company_ticker": company_tickers[i], + "page_number": page_number}) + i += 1 + print(f"Saved page {page_number}") + + + + + +if __name__ == "__main__": + main() diff --git a/stock-scraper/main.py b/stock-scraper/main.py new file mode 100644 index 0000000..44aba56 --- /dev/null +++ b/stock-scraper/main.py @@ -0,0 +1,92 @@ +import selenium +import selenium.webdriver +import selenium.webdriver.chrome.options +import selenium.webdriver.common.window + +# from selenium.webdriver.common.by import By +import time +import argparse + +def parse_arguments(): + """Parses command-line arguments.""" + parser = argparse.ArgumentParser(description="Stock Scraper Application") + # add an initial login parser + parser.add_argument( + "--initial-login", + action="store_true", + help="Perform initial login if specified.", + ) + # add parser for user-data-dir + parser.add_argument( + "--chrome-user-data-dir", + action="store", + default="_userdatachrome", + type=str, + help="Absolute path to the user data directory for chrome.", + ) + parser.add_argument( + "--chrome-profile-directory", + action="store", + default="Defaults", + type=str, + help="Name of the profile directory for chrome.", + ) + + return parser.parse_args() + +def main(): + args = parse_arguments() + + chrome(args) + #firefox(args) + +def firefox(args): + # Access the value of --initial-login + login_sleep_interval = 5 + if args.initial_login: + login_sleep_interval = 5 * 60 + + # Set up Firefox options + options = selenium.webdriver.FirefoxOptions() + + +def chrome(args): + # Access the value of --initial-login + login_sleep_interval = 5 + if args.initial_login: + login_sleep_interval = 5 * 60 + + # Set up Chrome options + options = selenium.webdriver.chrome.options.Options() + # used for using your current users' data directory + options.add_argument("--no-sandbox") + options.add_argument(f"--user-data-dir={args.chrome_user_data_dir}") + options.add_argument(f"--profile-directory={args.chrome_profile_directory}") + options.add_argument("--remote-debugging-port=9222") + + # Create a new instance of the Chrome driver + driver = selenium.webdriver.Chrome(options=options) + + # Store the ID of the original window + original_window = driver.current_window_handle + print(driver.get_cookies()) + driver.get("https://musaffa.com") + print(driver.get_cookies()) + # Navigate to the Google homepage + #new_window = driver.switch_to().new_window(selenium.webdriver.common.window.WindowType.WINDOW) + #print(new_window) + #new_window.maximize_window() + #print("maxed") + #new_window.get("https://musaffa.com") + + time.sleep(login_sleep_interval) # Pause for some time to either login manually + #new_window.close() + + # Close the browser + #driver.switch_to.window(original_window) + driver.quit() + + + +if __name__ == "__main__": + main() diff --git a/stock-scraper/pyproject.toml b/stock-scraper/pyproject.toml new file mode 100644 index 0000000..59c5757 --- /dev/null +++ b/stock-scraper/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "stock-scraper" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "selenium>=4.35.0", +] diff --git a/stock-scraper/ruff.toml b/stock-scraper/ruff.toml new file mode 100644 index 0000000..b5a1fb4 --- /dev/null +++ b/stock-scraper/ruff.toml @@ -0,0 +1,17 @@ +#target-version = "py310" +#src = ["src"] + +# Use a different line length. +line-length = 130 + +[lint.pydocstyle] +# Use Google-style docstrings. +convention = "google" + +[lint.isort] +# docs.astral.sh/ruff/settings/#lint_isort_split-on-trailing-comma +# split-on-trailing-comma = false + +[format] +# https://docs.astral.sh/ruff/settings/#format_skip-magic-trailing-comma +# skip-magic-trailing-comma = true diff --git a/stock-scraper/stockscraper/argparser.py b/stock-scraper/stockscraper/argparser.py new file mode 100644 index 0000000..e69de29 diff --git a/stock-scraper/stockscraper/musaffa.py b/stock-scraper/stockscraper/musaffa.py new file mode 100644 index 0000000..e69de29 diff --git a/stock-scraper/stockscraper/sourceinfo.py b/stock-scraper/stockscraper/sourceinfo.py new file mode 100644 index 0000000..1d7efcd --- /dev/null +++ b/stock-scraper/stockscraper/sourceinfo.py @@ -0,0 +1,46 @@ +import yaml +import os + + +class SourceInfo: + def __init__( + self, + url: str, + name: str, + destination_directory: str, + comment: str, + parsing_function, + expected_min_count: int, + check_intvl: int, + ): + self.url = url + self.comment = comment + self.parsing_function = parsing_function + self.destination_directory = destination_directory + self.name = name + self.expected_min_count = expected_min_count + self.check_intvl = check_intvl # in seconds + self._file_path = os.path.join(destination_directory, f"{name}.yaml") + self._tmp_file_path = os.path.join(destination_directory, "temp", f"{name}.yml") + self._last_check = self._get_last_check() + self._stocks = [] + + def _get_last_check(self): + if os.path.exists(self._file_path): + with open(self._file_path, "r") as f: + data = yaml.safe_load(f) + if data and "timestamp" in data: + return data["timestamp"] + return None # Return None if file doesn't exist or timestamp not found + + def create_temp_directory(self): + if os.path.exists(self.destination_directory): + os.makedirs(os.path.dirname(self._tmp_file_path), exist_ok=True) + + +class StocksInfo: + def __init__(self, symbol: str, name: str, exchange: str, country: str): + self.symbol = symbol + self.name = name + self.exchange = exchange + self.country = country diff --git a/stock-scraper/stockscraper/sources.py b/stock-scraper/stockscraper/sources.py new file mode 100644 index 0000000..abef005 --- /dev/null +++ b/stock-scraper/stockscraper/sources.py @@ -0,0 +1,11 @@ +from sourceinfo import SourceInfo + +SOURCES = [ + SourceInfo( + url="", + name="", + comment="", + parsing_function=None, + temporary_file_name="", + ), +] diff --git a/stock-scraper/uv.lock b/stock-scraper/uv.lock new file mode 100644 index 0000000..91c0581 --- /dev/null +++ b/stock-scraper/uv.lock @@ -0,0 +1,205 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + +[[package]] +name = "selenium" +version = "4.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "trio" }, + { name = "trio-websocket" }, + { name = "typing-extensions" }, + { name = "urllib3", extra = ["socks"] }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/67/9016942b5781843cfea6f5bc1383cea852d9fa08f85f55a0547874525b5c/selenium-4.35.0.tar.gz", hash = "sha256:83937a538afb40ef01e384c1405c0863fa184c26c759d34a1ebbe7b925d3481c", size = 907991, upload-time = "2025-08-12T15:46:40.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/ef/d0e033e1b3f19a0325ce03863b68d709780908381135fc0f9436dea76a7b/selenium-4.35.0-py3-none-any.whl", hash = "sha256:90bb6c6091fa55805785cf1660fa1e2176220475ccdb466190f654ef8eef6114", size = 9602106, upload-time = "2025-08-12T15:46:38.244Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "stock-scraper" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "selenium" }, +] + +[package.metadata] +requires-dist = [{ name = "selenium", specifier = ">=4.35.0" }] + +[[package]] +name = "trio" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776, upload-time = "2025-04-21T00:48:19.507Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194, upload-time = "2025-04-21T00:48:17.167Z" }, +] + +[[package]] +name = "trio-websocket" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "outcome" }, + { name = "trio" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[package.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" }, +]