mcmaster-scraper 0.1.0__py3-none-any.whl
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.
- mcmaster_scraper/__init__.py +5 -0
- mcmaster_scraper/_api/_text_parser.py +60 -0
- mcmaster_scraper/_api/scraper.py +56 -0
- mcmaster_scraper/_api/table_parser.py +49 -0
- mcmaster_scraper/_utils/event_loop_wrapper.py +55 -0
- mcmaster_scraper/_utils/page_provider.py +39 -0
- mcmaster_scraper/async_api.py +49 -0
- mcmaster_scraper/py.typed +0 -0
- mcmaster_scraper/sync_api.py +42 -0
- mcmaster_scraper-0.1.0.dist-info/METADATA +113 -0
- mcmaster_scraper-0.1.0.dist-info/RECORD +13 -0
- mcmaster_scraper-0.1.0.dist-info/WHEEL +4 -0
- mcmaster_scraper-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from fractions import Fraction
|
|
2
|
+
|
|
3
|
+
_STANDARD_HEADERS = {
|
|
4
|
+
"PART_NUMBER": "Part Number",
|
|
5
|
+
"PRICING": "Price",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_header_text(col_id: int, meta: dict):
|
|
10
|
+
# Column Id -> Column Metadata
|
|
11
|
+
column_metas = meta["ColumnIdToMetadata"]
|
|
12
|
+
column_meta = column_metas[col_id]
|
|
13
|
+
|
|
14
|
+
header_type = column_meta.get("Type")
|
|
15
|
+
if isinstance(header_type, str) and header_type in _STANDARD_HEADERS:
|
|
16
|
+
return _STANDARD_HEADERS[header_type]
|
|
17
|
+
else:
|
|
18
|
+
# Column Metadata -> Header
|
|
19
|
+
return _extract_text(column_meta)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_cell_text(cell_id: int, meta: dict):
|
|
23
|
+
# Cell ID -> Value Metadata ID
|
|
24
|
+
cell_metas = meta["CellIdToCellMetadata"]
|
|
25
|
+
cell_meta = cell_metas[cell_id]
|
|
26
|
+
value_meta_id = cell_meta["ValueMetadataIds"][0]
|
|
27
|
+
|
|
28
|
+
# Value Metadata ID -> Value Metadata
|
|
29
|
+
value_metas = meta["ValueMetadataIdToValueMetadata"]
|
|
30
|
+
value_meta = value_metas[value_meta_id]
|
|
31
|
+
|
|
32
|
+
# Value Metadata -> Value
|
|
33
|
+
return _extract_text(value_meta)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _extract_text(meta_item: dict):
|
|
37
|
+
components = meta_item["Name"]["Components"]
|
|
38
|
+
text = " ".join(c["Text"] for c in components)
|
|
39
|
+
|
|
40
|
+
return _parse_number(text)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _parse_number(text: str):
|
|
44
|
+
t = text.replace('"', "").strip()
|
|
45
|
+
|
|
46
|
+
if t == "":
|
|
47
|
+
return t
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
return float(t)
|
|
51
|
+
except ValueError:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
fraction = sum(Fraction(part) for part in t.split())
|
|
56
|
+
return float(fraction)
|
|
57
|
+
except ValueError:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
return text
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from .._utils.page_provider import get_page
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def get_product_api_response(url: str) -> dict:
|
|
11
|
+
if not _is_valid_url(url):
|
|
12
|
+
raise ValueError("Not a McMaster-Carr URL")
|
|
13
|
+
|
|
14
|
+
logger.info("Finding API for product page...")
|
|
15
|
+
# Using Playwright because the API can only be discovered by loading the JavaScript
|
|
16
|
+
page = await get_page()
|
|
17
|
+
|
|
18
|
+
await page.goto(url)
|
|
19
|
+
|
|
20
|
+
# If the JSON is too large, the response will be evicted
|
|
21
|
+
# from the inspector cache before we can access it
|
|
22
|
+
|
|
23
|
+
# As a workaround, we can navigate to the API URL
|
|
24
|
+
# and extract the response from the page's body
|
|
25
|
+
|
|
26
|
+
product_api = "**/ProdPageWebPart.aspx?**"
|
|
27
|
+
async with page.expect_request(product_api, timeout=5000) as request:
|
|
28
|
+
value = await request.value
|
|
29
|
+
api_url = value.url
|
|
30
|
+
|
|
31
|
+
logger.info("Getting API response...")
|
|
32
|
+
await page.goto(api_url)
|
|
33
|
+
|
|
34
|
+
res = await page.locator("body").text_content()
|
|
35
|
+
assert res is not None
|
|
36
|
+
|
|
37
|
+
data = _extract_json_from_response(res)
|
|
38
|
+
|
|
39
|
+
await page.close()
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _extract_json_from_response(res: str) -> dict:
|
|
44
|
+
start = res.find("{")
|
|
45
|
+
end = res.rfind("}")
|
|
46
|
+
|
|
47
|
+
if start == -1 or end == -1:
|
|
48
|
+
raise ValueError("No JSON found in API response")
|
|
49
|
+
|
|
50
|
+
json_str = res[start : end + 1]
|
|
51
|
+
return json.loads(json_str)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_valid_url(url: str) -> bool:
|
|
55
|
+
pattern = re.compile(r"^https?://(www\.)?mcmaster\.com(/\S*)?$")
|
|
56
|
+
return bool(pattern.match(url))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from pandas import DataFrame
|
|
2
|
+
|
|
3
|
+
from ._text_parser import get_cell_text, get_header_text
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_product_tables(json: dict) -> dict[str, DataFrame]:
|
|
7
|
+
tables = _find_pivot_tables(json)
|
|
8
|
+
dataframes = {k: _parse_pivot_table(v) for k, v in tables.items()}
|
|
9
|
+
return dataframes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _find_pivot_tables(root: dict) -> dict:
|
|
13
|
+
stack = [root]
|
|
14
|
+
while stack:
|
|
15
|
+
node = stack.pop()
|
|
16
|
+
|
|
17
|
+
if isinstance(node, dict):
|
|
18
|
+
if node.get("Name") == "ProductPresentations":
|
|
19
|
+
return {
|
|
20
|
+
product["Display"]["Title"]: product["Table"]
|
|
21
|
+
for product in node["Data"]
|
|
22
|
+
}
|
|
23
|
+
else:
|
|
24
|
+
stack.extend(node.values())
|
|
25
|
+
|
|
26
|
+
elif isinstance(node, list):
|
|
27
|
+
stack.extend(node)
|
|
28
|
+
|
|
29
|
+
raise KeyError("The McMaster URL provided does not have a visible product table.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parse_pivot_table(table: dict) -> DataFrame:
|
|
33
|
+
col_ids = table["ColumnIds"]
|
|
34
|
+
rows = table["Rows"]
|
|
35
|
+
meta = table["Metadata"]
|
|
36
|
+
|
|
37
|
+
# Build headers
|
|
38
|
+
headers = [get_header_text(col_id, meta) for col_id in col_ids]
|
|
39
|
+
|
|
40
|
+
# Build row data
|
|
41
|
+
def get_row_data(row: dict):
|
|
42
|
+
cell_ids = row["ColumnIdToCellIdMap"]
|
|
43
|
+
return {
|
|
44
|
+
header: get_cell_text(cell_ids[col_id], meta)
|
|
45
|
+
for col_id, header in zip(col_ids, headers)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
data = [get_row_data(row) for row in rows]
|
|
49
|
+
return DataFrame(data)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
import threading
|
|
4
|
+
from asyncio import AbstractEventLoop, CancelledError
|
|
5
|
+
from concurrent.futures import Future
|
|
6
|
+
from typing import Any, Coroutine, TypeVar, Union
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
_loop: Union[AbstractEventLoop, None] = None
|
|
11
|
+
_started = threading.Event()
|
|
12
|
+
_lock = threading.Lock()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def run_in_loop_async(func: Coroutine[Any, Any, T]) -> T:
|
|
16
|
+
c_future = _run_in_loop(func)
|
|
17
|
+
a_future = asyncio.wrap_future(c_future)
|
|
18
|
+
try:
|
|
19
|
+
return await a_future
|
|
20
|
+
except CancelledError:
|
|
21
|
+
c_future.cancel()
|
|
22
|
+
raise
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_in_loop_sync(func: Coroutine[Any, Any, T]) -> T:
|
|
26
|
+
return _run_in_loop(func).result()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _run_in_loop(func: Coroutine[Any, Any, T]) -> Future[T]:
|
|
30
|
+
loop = _ensure_loop()
|
|
31
|
+
return asyncio.run_coroutine_threadsafe(func, loop)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ensure_loop() -> AbstractEventLoop:
|
|
35
|
+
global _loop
|
|
36
|
+
with _lock:
|
|
37
|
+
if _loop is None:
|
|
38
|
+
t = threading.Thread(target=_run_loop, daemon=True)
|
|
39
|
+
t.start()
|
|
40
|
+
_started.wait()
|
|
41
|
+
|
|
42
|
+
assert _loop is not None
|
|
43
|
+
return _loop
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_loop():
|
|
47
|
+
global _loop
|
|
48
|
+
if sys.platform.startswith("win"):
|
|
49
|
+
loop = asyncio.ProactorEventLoop()
|
|
50
|
+
else:
|
|
51
|
+
loop = asyncio.SelectorEventLoop()
|
|
52
|
+
asyncio.set_event_loop(loop)
|
|
53
|
+
_loop = loop
|
|
54
|
+
_started.set()
|
|
55
|
+
loop.run_forever()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from playwright.async_api import (
|
|
5
|
+
Browser,
|
|
6
|
+
BrowserContext,
|
|
7
|
+
Page,
|
|
8
|
+
Playwright,
|
|
9
|
+
async_playwright,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
_browser: Union[Browser, None] = None
|
|
13
|
+
_browser_context: Union[BrowserContext, None] = None
|
|
14
|
+
_playwright: Union[Playwright, None] = None
|
|
15
|
+
_lock = asyncio.Lock()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def _ensure_browser_context() -> BrowserContext:
|
|
19
|
+
global _browser, _browser_context, _playwright
|
|
20
|
+
|
|
21
|
+
async with _lock:
|
|
22
|
+
if _browser_context:
|
|
23
|
+
return _browser_context
|
|
24
|
+
|
|
25
|
+
_playwright = await async_playwright().start()
|
|
26
|
+
assert _playwright
|
|
27
|
+
|
|
28
|
+
_browser = await _playwright.chromium.launch()
|
|
29
|
+
assert _browser
|
|
30
|
+
|
|
31
|
+
_browser_context = await _browser.new_context()
|
|
32
|
+
assert _browser_context
|
|
33
|
+
|
|
34
|
+
return _browser_context
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def get_page() -> Page:
|
|
38
|
+
browser_context = await _ensure_browser_context()
|
|
39
|
+
return await browser_context.new_page()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from asyncio import create_task, gather
|
|
3
|
+
|
|
4
|
+
import diskcache as dc
|
|
5
|
+
import platformdirs
|
|
6
|
+
from pandas import DataFrame, concat
|
|
7
|
+
|
|
8
|
+
from ._api.scraper import get_product_api_response
|
|
9
|
+
from ._api.table_parser import get_product_tables
|
|
10
|
+
from ._utils.event_loop_wrapper import run_in_loop_async
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def get_products_from_url(url: str, refresh: bool = False) -> DataFrame:
|
|
14
|
+
"""Gets product tables from a McMaster-Carr URL.
|
|
15
|
+
|
|
16
|
+
See Also
|
|
17
|
+
--------
|
|
18
|
+
sync_api.get_products_from_url
|
|
19
|
+
"""
|
|
20
|
+
cache_dir = platformdirs.user_cache_dir(
|
|
21
|
+
appname="mcmaster-scraper", appauthor=False, ensure_exists=True
|
|
22
|
+
)
|
|
23
|
+
cache = dc.Cache(cache_dir, eviction_policy="least-recently-used")
|
|
24
|
+
key = hashlib.md5(url.encode()).hexdigest()
|
|
25
|
+
|
|
26
|
+
if key in cache and not refresh:
|
|
27
|
+
json = cache[key]
|
|
28
|
+
else:
|
|
29
|
+
json = await run_in_loop_async(get_product_api_response(url))
|
|
30
|
+
cache[key] = json
|
|
31
|
+
|
|
32
|
+
tables = get_product_tables(json)
|
|
33
|
+
tables_with_product_type = [
|
|
34
|
+
table.assign(**{"Product Type": product}) for product, table in tables.items()
|
|
35
|
+
]
|
|
36
|
+
return concat(tables_with_product_type, ignore_index=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def get_products_from_urls(
|
|
40
|
+
urls: list[str], refresh: bool = False
|
|
41
|
+
) -> list[DataFrame]:
|
|
42
|
+
"""Gets product tables from a list of McMaster-Carr URLs.
|
|
43
|
+
|
|
44
|
+
See Also
|
|
45
|
+
--------
|
|
46
|
+
sync_api.get_products_from_urls
|
|
47
|
+
"""
|
|
48
|
+
tasks = [create_task(get_products_from_url(url, refresh)) for url in urls]
|
|
49
|
+
return await gather(*tasks)
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pandas import DataFrame
|
|
2
|
+
|
|
3
|
+
from . import async_api
|
|
4
|
+
from ._utils.event_loop_wrapper import run_in_loop_sync
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_products_from_url(url: str, refresh: bool = False) -> DataFrame:
|
|
8
|
+
"""Gets product tables from a McMaster-Carr URL.
|
|
9
|
+
|
|
10
|
+
If there are multiple product tables, they will be merged,
|
|
11
|
+
and an additional "Product Type" column will be added.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
url : str
|
|
16
|
+
The URL to scrape.
|
|
17
|
+
Must be a valid McMaster-Carr URL.
|
|
18
|
+
The product tables must be visible on the webpage.
|
|
19
|
+
refresh : bool, optional
|
|
20
|
+
Whether to refresh the cached data. Default is False.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
DataFrame
|
|
25
|
+
A pandas DataFrame containing the combined product tables.
|
|
26
|
+
|
|
27
|
+
Raises
|
|
28
|
+
------
|
|
29
|
+
ValueError
|
|
30
|
+
If the URL is not a valid McMaster-Carr URL.
|
|
31
|
+
"""
|
|
32
|
+
return run_in_loop_sync(async_api.get_products_from_url(url, refresh))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_products_from_urls(urls: list[str], refresh: bool = False) -> list[DataFrame]:
|
|
36
|
+
"""Gets product tables from a list of McMaster-Carr URLs.
|
|
37
|
+
|
|
38
|
+
See Also
|
|
39
|
+
--------
|
|
40
|
+
get_products_from_url
|
|
41
|
+
"""
|
|
42
|
+
return run_in_loop_sync(async_api.get_products_from_urls(urls, refresh))
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcmaster-scraper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fetch product tables from a McMaster-Carr URL as a DataFrame for complex filtering and calculations
|
|
5
|
+
Keywords: mcmaster,mcmaster-carr,scraper,extractor,parser,fetcher,part,catalog,product
|
|
6
|
+
Author: Alex
|
|
7
|
+
Author-email: Alex <thedjchidev@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Manufacturing
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Requires-Dist: diskcache>=5.6.3
|
|
23
|
+
Requires-Dist: pandas>=2.3.3
|
|
24
|
+
Requires-Dist: platformdirs>=3.5.1
|
|
25
|
+
Requires-Dist: playwright>=1.14.0
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Project-URL: github, https://github.com/thedjchi/mcmaster-scraper
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# McMaster-Scraper
|
|
31
|
+
|
|
32
|
+
A Python library for fetching product tables from a [McMaster-Carr](https://www.mcmaster.com) URL
|
|
33
|
+
as a [DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html) for complex filtering and
|
|
34
|
+
calculations.
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- Caches data locally to speed up future calls
|
|
43
|
+
- Supports both sync/async APIs
|
|
44
|
+
- Works in Python files and Jupyter notebooks
|
|
45
|
+
- Includes convenience functions to quickly retrieve product tables from multiple URLs
|
|
46
|
+
- Typed functions for type-checking compatibility
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
McMaster-Scraper is available on PyPi:
|
|
51
|
+
|
|
52
|
+
`pip install mcmaster-scraper`
|
|
53
|
+
|
|
54
|
+
McMaster-Scraper requires [Playwright](https://playwright.dev/python) to fetch the product tables. It is already
|
|
55
|
+
included as a dependency. However, you will need to install the browsers manually:
|
|
56
|
+
|
|
57
|
+
`playwright install`
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
To use the Sync API, import the `sync_api` module and call `get_products_from_url(s)`:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
from mcmaster_scraper.sync_api import get_products_from_url
|
|
65
|
+
|
|
66
|
+
url = "https://www.mcmaster.com/products/screws/socket-head-screws-2~/steel-socket-head-screws~~/"
|
|
67
|
+
data = get_products_from_url(url) # Returns a DataFrame with all the products from the URL
|
|
68
|
+
|
|
69
|
+
... # Do stuff with the DataFrame (filter, perform calculations, etc.)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Using the Async API is similar, import the `async_api` module and `await` the function call:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
from mcmaster_scraper.async_api import get_products_from_url
|
|
76
|
+
|
|
77
|
+
url = "https://www.mcmaster.com/products/screws/socket-head-screws-2~/steel-socket-head-screws~~/"
|
|
78
|
+
data = await get_products_from_url(url) # Returns a DataFrame with all the products from the URL
|
|
79
|
+
|
|
80
|
+
... # Do stuff with the DataFrame (filter, perform calculations, etc.)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Docs
|
|
84
|
+
|
|
85
|
+
### API Reference
|
|
86
|
+
|
|
87
|
+
The API reference can be found on [GitHub Pages](https://thedjchi.github.io/mcmaster-scraper/mcmaster_scraper.html).
|
|
88
|
+
|
|
89
|
+
### Examples
|
|
90
|
+
|
|
91
|
+
An example script can be found
|
|
92
|
+
in [docs/example.py](https://github.com/thedjchi/mcmaster-scraper/blob/master/docs/example.py).
|
|
93
|
+
|
|
94
|
+
## Disclaimer
|
|
95
|
+
|
|
96
|
+
This library is for responsible data extraction only. Do not:
|
|
97
|
+
|
|
98
|
+
- Scrape beyond reasonable rates
|
|
99
|
+
- Violate Terms of Service
|
|
100
|
+
- Circumvent access controls
|
|
101
|
+
- Use data for unauthorized commercial purposes
|
|
102
|
+
|
|
103
|
+
## Legal Notice
|
|
104
|
+
|
|
105
|
+
This library is provided as-is. Authors are not liable for any legal, technical, or business consequences resulting from
|
|
106
|
+
misuse of this library. Users assume full responsibility for compliance with applicable laws, regulations, and website
|
|
107
|
+
policies.
|
|
108
|
+
|
|
109
|
+
**By using this library, you acknowledge and agree to these responsibilities.**
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
[MIT](https://github.com/thedjchi/mcmaster-scraper/blob/master/LICENSE)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
mcmaster_scraper/__init__.py,sha256=ZS1B55NxS-IgvmJg4Z__BQxNVGp9Jw_eHLl0Ued2M7k,75
|
|
2
|
+
mcmaster_scraper/_api/_text_parser.py,sha256=D3QD36I5LrAJCM35VB5KW33yWb_0NSKOLj8atB1jsHM,1461
|
|
3
|
+
mcmaster_scraper/_api/scraper.py,sha256=MNel-80oMD4aF-tuy6UyPAFi9yXcEAQZdH-KseJ3GGk,1510
|
|
4
|
+
mcmaster_scraper/_api/table_parser.py,sha256=e26BS8d5SVPIFJIsIujpto3AYoYyOVhY5fWpSHK_0l4,1405
|
|
5
|
+
mcmaster_scraper/_utils/event_loop_wrapper.py,sha256=UkYw5OmjiQ-wq1sf_MZ8SQAXZh6CHkSsZstCjGxWh4U,1323
|
|
6
|
+
mcmaster_scraper/_utils/page_provider.py,sha256=nUFaKSQZvovM_RlVWzBRKpy5XgMhdI76umELaW_OuHE,929
|
|
7
|
+
mcmaster_scraper/async_api.py,sha256=dusD84Cn8blWQWjy4jUmlShQj2qtoNDzVHqE9mPeqOI,1484
|
|
8
|
+
mcmaster_scraper/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
mcmaster_scraper/sync_api.py,sha256=OZFmmar0y09Ne2rAs65CpUl3bKyIhs6GDZEvGYNVSt8,1195
|
|
10
|
+
mcmaster_scraper-0.1.0.dist-info/licenses/LICENSE,sha256=dT4IySfb5VBHiFbHfgF7UXboXtMlw273PdJeyBA8U2A,1065
|
|
11
|
+
mcmaster_scraper-0.1.0.dist-info/WHEEL,sha256=iCTolw4aw2dP3yfM-EQCGTDsFCXL_ymmbYnBRVH7plA,81
|
|
12
|
+
mcmaster_scraper-0.1.0.dist-info/METADATA,sha256=JxgVTwXozhDCWukDRvcfWEuUc-TgyZl06Xdp7WrFARM,4101
|
|
13
|
+
mcmaster_scraper-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thedjchi
|
|
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.
|