repo: push
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Akademiya
|
||||
books/
|
||||
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.13
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.analysis.autoImportCompletions": true
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 tretrauit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Akademiya
|
||||
|
||||
Unofficial API client written in Python for a certain book library.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install git+https://git.tretrauit.me/tretrauit/akademiya
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
+ [x] Authentication
|
||||
+ [x] Look up books
|
||||
+ [x] Borrow books
|
||||
+ [x] Download (encrypted) books
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
4
akademiya/__init__.py
Normal file
4
akademiya/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from akademiya.client import Client
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__all__ = ["Client"]
|
||||
116
akademiya/__main__.py
Normal file
116
akademiya/__main__.py
Normal file
@ -0,0 +1,116 @@
|
||||
from getpass import getpass
|
||||
from pathlib import Path
|
||||
from akademiya import __version__, Client
|
||||
from traceback import print_exc
|
||||
|
||||
# Import readline if available (Unix-like systems)
|
||||
try:
|
||||
import readline # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
base_path = Path("books")
|
||||
base_path.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def lookup_books(client: Client, name: str):
|
||||
print(f"Looking up books with the name '{name}'...")
|
||||
books = client.lookup_books(name)
|
||||
for book in books:
|
||||
print(f"Book: {book.product_title} (ID: {book.ID})")
|
||||
print(f" + Author: {book.creator_names}")
|
||||
|
||||
|
||||
def download_book(client: Client, book_id: int):
|
||||
print(f"Fetching book with ID {book_id}...")
|
||||
book = client.get_book_info(book_id)
|
||||
print(f"Downloading book '{book.product_title}'...")
|
||||
pdf_bytes = client.get_book_pdf(book)
|
||||
if pdf_bytes is None:
|
||||
print("The server didn't return any PDF data.")
|
||||
return
|
||||
pdf_path = base_path / f"{book.ID}.pdf"
|
||||
with pdf_path.open("wb") as f:
|
||||
f.write(pdf_bytes)
|
||||
print(f"Book downloaded to '{pdf_path.resolve()}'")
|
||||
|
||||
|
||||
def borrow_book(client: Client, book_id: int):
|
||||
print(f"Borrowing book with ID {book_id}...")
|
||||
if client.is_borrowing(book_id=book_id):
|
||||
print("You are already borrowing this book.")
|
||||
return
|
||||
client.borrow_book(book_id)
|
||||
print("Book borrowed successfully.")
|
||||
|
||||
|
||||
def user_info(client: Client):
|
||||
print(f"User ID: {client.user.id}")
|
||||
print(f"Username: {client.user.username}")
|
||||
print(f"Full Name: {client.user.full_name}")
|
||||
print(f"Avatar URL: {client.user.avatar_url}")
|
||||
print(f"Token: {client.user.token}")
|
||||
|
||||
|
||||
def main():
|
||||
print(
|
||||
f"Akademiya CLI v{__version__} - https://git.tretrauit.me/tretrauit/akademiya"
|
||||
)
|
||||
print("Welcome to Akademiya CLI, an unofficial client for a book library.")
|
||||
print(
|
||||
"This client is no way affiliated with the official client itself, and should be used for educational purposes only."
|
||||
)
|
||||
client = Client()
|
||||
print()
|
||||
print("Authorization is required to access the library.")
|
||||
username = input("Username: ")
|
||||
password = getpass("Password (doesn't show on the console): ")
|
||||
try:
|
||||
client.login(username, password)
|
||||
except Exception as e:
|
||||
print(f"An error occurred while logging in: {e}")
|
||||
return
|
||||
print(f"Logged in as {client.user.full_name} (ID: {client.user.id})")
|
||||
print("Type 'help' for a list of available commands.")
|
||||
# Very simple command handling but idgaf.
|
||||
while True:
|
||||
try:
|
||||
command_str = input("> ")
|
||||
base_command = command_str.split(" ", 1)[0]
|
||||
match base_command:
|
||||
case "exit":
|
||||
print("Exiting...")
|
||||
break
|
||||
case "help":
|
||||
print("Available commands:")
|
||||
print("exit - Exits the program")
|
||||
print("help - Shows this help message")
|
||||
print("lookup <book name> - Lookup books by name")
|
||||
print("download <book ID> - Download a book by ID")
|
||||
print("borrow <book ID> - Borrow a book by ID")
|
||||
print("userinfo - Show the current user information")
|
||||
case "lookup":
|
||||
_, name = command_str.split(" ", 1)
|
||||
lookup_books(client, name)
|
||||
case "download":
|
||||
_, book_id = command_str.split(" ", 1)
|
||||
download_book(client, int(book_id))
|
||||
case "borrow":
|
||||
_, book_id = command_str.split(" ", 1)
|
||||
borrow_book(client, int(book_id))
|
||||
case "userinfo":
|
||||
user_info(client)
|
||||
case _:
|
||||
print(
|
||||
"Unknown command, type 'help' for a list of available commands."
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("Exiting...")
|
||||
break
|
||||
except Exception:
|
||||
print("An error occurred while executing the provided command.")
|
||||
print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
224
akademiya/client.py
Normal file
224
akademiya/client.py
Normal file
@ -0,0 +1,224 @@
|
||||
import inspect
|
||||
import requests
|
||||
from akademiya import constants
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
@dataclass
|
||||
class BookPdf:
|
||||
"""
|
||||
The PDF file of the book.
|
||||
|
||||
Only useful fields are covered, the rest are ignored.
|
||||
"""
|
||||
|
||||
FileTitle: str
|
||||
ID: int
|
||||
Position: int
|
||||
Type: str
|
||||
Url: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(self, data: dict) -> "Book":
|
||||
# Ty https://stackoverflow.com/a/55096964
|
||||
return self(
|
||||
**{k: v for k, v in data.items() if k in inspect.signature(self).parameters}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Book:
|
||||
"""
|
||||
The product, or in our case the book.
|
||||
|
||||
Only useful fields are covered, the rest are ignored.
|
||||
"""
|
||||
|
||||
Author: str | None
|
||||
CoverPicName: str
|
||||
ID: int
|
||||
Status: int
|
||||
# Category fields
|
||||
category_id: int
|
||||
category_name: str | None
|
||||
category_type: str | None
|
||||
collection_name: int
|
||||
creator_names: str | None
|
||||
# Product fields
|
||||
product_code: str | None
|
||||
product_cover: str
|
||||
product_description: str | None
|
||||
product_pdf: list[BookPdf] | None
|
||||
product_title: str
|
||||
publisher_name: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(self, data: dict) -> "Book":
|
||||
# Ty https://stackoverflow.com/a/55096964
|
||||
return_val = self(
|
||||
**{k: v for k, v in data.items() if k in inspect.signature(self).parameters}
|
||||
)
|
||||
if (data["product_pdf"]) is not None:
|
||||
return_val.product_pdf = [
|
||||
BookPdf.from_dict(pdf) for pdf in data["product_pdf"]
|
||||
]
|
||||
return return_val
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
full_name: str
|
||||
id: int
|
||||
avatar_url: str
|
||||
token: str
|
||||
username: str
|
||||
|
||||
|
||||
class Client:
|
||||
"""
|
||||
Client for interacting with the book library server.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._session = requests.Session()
|
||||
# Update User-Agent to mask as the official client (although it's not necessary)
|
||||
self._session.headers.update({"User-Agent": constants.USER_AGENT})
|
||||
self._app_version: float = None
|
||||
self.user: User = None
|
||||
self.set_app_version()
|
||||
|
||||
def borrow_book(self, book_id: int):
|
||||
rsp = self._session.get(
|
||||
f"{constants.ENDPOINT}/API/cbs/User/Payment/{self.user.token}/{book_id}/{constants.MAGIC_KEY}/1/1/"
|
||||
)
|
||||
rsp.raise_for_status()
|
||||
result = rsp.json()["BookBorrowsResult"]
|
||||
if result["Message"] == "Mượn sách thành công.":
|
||||
return
|
||||
raise Exception(f"Failed to borrow book: {result['Message']}")
|
||||
|
||||
def get_book_info(self, book_id: int):
|
||||
rsp = self._session.get(
|
||||
f"{constants.ENDPOINT}/API/cbs/products/getinfo.json/{book_id}/1/"
|
||||
)
|
||||
rsp.raise_for_status()
|
||||
# GetProductByIDResult is the root
|
||||
result = rsp.json()["GetProductByIDResult"]
|
||||
if result["ValidateMessage"]["Message"] != "Lấy dữ liệu thành công":
|
||||
raise Exception(f"Lookup failed: {result['ValidateMessage']['Message']}")
|
||||
return Book.from_dict(result)
|
||||
|
||||
def get_book_pdf(self, book: Book):
|
||||
"""
|
||||
Downloads the PDF of the given book.
|
||||
|
||||
Args:
|
||||
book (Book): The book to download the PDF from.
|
||||
|
||||
Returns:
|
||||
bytes: The PDF data if successful, None otherwise.
|
||||
"""
|
||||
if book.product_pdf is None:
|
||||
return None
|
||||
if not self.is_borrowing(book.ID):
|
||||
raise Exception("You must borrow the book before downloading the PDF.")
|
||||
pdf = book.product_pdf[0]
|
||||
rsp = self._session.get(
|
||||
f"{pdf.Url}{book.product_code}/{constants.MAGIC_KEY}/APPWINDOW.1.0/{self.user.token}/{self.user.username}/1/1/"
|
||||
)
|
||||
rsp.raise_for_status()
|
||||
# Comment so Pylance can stfu
|
||||
if not isinstance(rsp.content, bytes):
|
||||
raise Exception("Expected bytes, got something else.")
|
||||
try:
|
||||
data = rsp.json()
|
||||
raise Exception(f"Failed to get PDF: {data['ValidateMessage']['Message']}")
|
||||
except Exception:
|
||||
pass
|
||||
return rsp.content
|
||||
|
||||
def is_borrowing(self, book_id: int) -> bool:
|
||||
"""
|
||||
Checks if the user is borrowing a book.
|
||||
|
||||
Args:
|
||||
book_id (int): The ID of the book to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the user is borrowing the book, False otherwise."""
|
||||
rsp = self._session.get(
|
||||
f"{constants.ENDPOINT}/API/cbs/User/Payment/Date/{self.user.username}/{book_id}/"
|
||||
)
|
||||
rsp.raise_for_status()
|
||||
result = rsp.json()["BorrowsDateResult"]
|
||||
if result["ValidateMessage"]["Message"] == "Sách chưa mượn hoặc đã trả!":
|
||||
return False
|
||||
return True
|
||||
|
||||
def login(self, username: str, password: str):
|
||||
"""
|
||||
Logs in the user with the given credentials.
|
||||
|
||||
You must call this method before calling any other methods.
|
||||
|
||||
Args:
|
||||
username (str): The username.
|
||||
password (str): The password.
|
||||
"""
|
||||
# Why did they put the username & password in the URL? :)
|
||||
rsp = self._session.get(
|
||||
f"{constants.ENDPOINT}/Api/loginrs/{username}/{password}/WINDOWS/"
|
||||
)
|
||||
rsp.raise_for_status()
|
||||
data = rsp.json()
|
||||
result = data["GetPatronByAccountResult"]
|
||||
if result["ValidateMessage"]["Message"] != "Đăng nhập thành công!":
|
||||
raise Exception(f"Login failed: {result['ValidateMessage']['Message']}")
|
||||
self.user = User(
|
||||
full_name=result["FullName"],
|
||||
id=result["Id"],
|
||||
avatar_url=result["ImageUrl"],
|
||||
token=result["token"],
|
||||
username=username,
|
||||
)
|
||||
|
||||
def lookup_books(self, name: str):
|
||||
"""
|
||||
Looks up books by name.
|
||||
|
||||
Although this method provides book information, if you need more detailed information for a
|
||||
specific book then you should use `get_book_info` along with the given book ID.
|
||||
|
||||
Args:
|
||||
name (str): The name to search for.
|
||||
|
||||
Returns:
|
||||
list[Product]: A list of products (books) that match the search query.
|
||||
"""
|
||||
# What in the fuck is /0/0/1/20/1 :)
|
||||
rsp = self._session.get(
|
||||
f"{constants.ENDPOINT}/API/cbs/products/Search/{quote(name)}/0/0/1/20/1/{self.user.token}/"
|
||||
)
|
||||
rsp.raise_for_status()
|
||||
# SearchEngResult is the root
|
||||
result = rsp.json()["SearchEngResult"]
|
||||
if result["ValidateMessage"]["Message"] != "Tra cứu dữ liệu thành công":
|
||||
raise Exception(f"Lookup failed: {result['ValidateMessage']['Message']}")
|
||||
products: list[Book] = []
|
||||
for product_dict in result["ProductList"]:
|
||||
product = Book.from_dict(product_dict)
|
||||
products.append(product)
|
||||
return products
|
||||
|
||||
def set_app_version(self):
|
||||
"""
|
||||
Sets the app version to the latest version available on the server.
|
||||
|
||||
This method is called automatically when the client is created.
|
||||
"""
|
||||
rsp = self._session.get(
|
||||
f"{constants.ENDPOINT}/API/cbs/products/getversionapp/windows/"
|
||||
)
|
||||
rsp.raise_for_status()
|
||||
self._app_version = float(rsp.json()["Result"])
|
||||
3
akademiya/constants.py
Normal file
3
akademiya/constants.py
Normal file
@ -0,0 +1,3 @@
|
||||
USER_AGENT = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; Tablet PC 2.0)"
|
||||
ENDPOINT = "http://bookworm.vnu.edu.vn"
|
||||
MAGIC_KEY = "CA72dacfce21b55c"
|
||||
9
pyproject.toml
Normal file
9
pyproject.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[project]
|
||||
name = "akademiya"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"requests>=2.32.3",
|
||||
]
|
||||
79
uv.lock
generated
Normal file
79
uv.lock
generated
Normal file
@ -0,0 +1,79 @@
|
||||
version = 1
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "akademiya"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "requests", specifier = ">=2.32.3" }]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 },
|
||||
]
|
||||
|
||||
[[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 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
|
||||
]
|
||||
Reference in New Issue
Block a user