import all

This commit is contained in:
ph0sgene committed 2025-10-09 12:35:28 -07:00
commit 881baaa1ed
25 files changed
+1779

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
logs/
.env
__pycache__/
*.pyc
.vscode/
.idea/
.DS_Store
*.db
*.sqlite3
*.log
poetry.lock
bin/build/
.zip
snippet.py
doc/square-src/square-python-sdk-43.0.2.20250820/
doc/square-src/
doc/pw-inventory/pw-inventory.zip
doc/pw-inventory/SQUARE_inventory_forautomation_2025-06-18-0139.csv
doc/pw-inventory/square_inventory_export_2025-06-18-0137.xlsx
square-src.code-workspace
+373
View File
@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
+11
View File
@@ -0,0 +1,11 @@
# Edderkop
environment: poetry
code style: black
built with
- aiohttp
- squareup
- apscheduler
- aiosqlite
+4
View File
@@ -0,0 +1,4 @@
- glue shit together
- test sq
- test inserting brickowl into sq
- test kopling
+11
View File
@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "../kopling"
}
],
"settings": {}
}
+14
View File
@@ -0,0 +1,14 @@
import src.test_server
class edderkop:
def __init__(self):
self.name = "Edderkop"
self.version = "0.0.1"
self.description = """
Edderkop is an application designed to interact with the Brickset API and directly with Square.
It allows users to search for items, manage collections, and perform various operations related to bricksets.
"""
def run(self):
print(f"{self.name} v{self.version} is running.")
+35
View File
@@ -0,0 +1,35 @@
[project]
name = "edderkop"
version = "0.1.0"
description = ""
authors = [
{name = "CF", email = "chris-forst@pm.me"}
]
license = {text = "MIT"}
readme = "README.md"
requires-python = ">=3.13,<3.14"
dependencies = [
"aiohttp (>=3.12.13,<4.0.0)",
"dotenv (>=0.9.9,<0.10.0)",
"pyinstaller (>=6.14.1,<7.0.0)",
"squareup(>=43.0.0.20250618,<44.0.0.0)",
"asyncio-throttle (>=1.0.2,<2.0.0)",
"requests (>=2.32.4,<3.0.0)",
"pytest (>=8.4.2,<9.0.0)",
"apscheduler (>=3.11.0,<4.0.0)",
"aiosqlite (>=0.21.0,<0.22.0)",
]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
package-mode = false
[tool.bricksetapi]
version = "0.1.0"
[tool.abricksetapi]
version = "0.1.0"
+19
View File
@@ -0,0 +1,19 @@
from setuptools import setup
setup(
name='Edderkop',
version='0.1',
description='Bricket + Square integration application',
author='CF',
author_email='chris-forst@pm.me',
packages=['src'],
package_dir={'': 'src'},
install_requires=[
# List your project dependencies here
],
entry_points={
'console_scripts': [
'main=main:main', # Adjust this line based on your main function
],
},
)
+120
View File
@@ -0,0 +1,120 @@
import aiohttp
import json
import dotenv
import os
import importlib.metadata
dotenv.load_dotenv()
api_key = os.getenv("BRICKOWL")
abrickowl_version = importlib.metadata.version("abrickowlapi")
class ABrickowlAPI:
def __init__(self, value):
self.user_agent = f"abrickowlapi/{abrickowl_version}"
self.headers = {"content-type": "application/x-www-form-urlencoded"}
self.base_url = "https://api.brickowl.com/v1/"
self.session = aiohttp.ClientSession()
return self
def set_valid(self, set_id: int):
"""
Verify if a LEGO set ID is valid.
"""
if len(set_id) == 5:
return True
else:
return False
@classmethod
async def catalog_list(self):
"""
Fetch the list of LEGO sets from the Brickowl API.
"""
async with self.session.get(
f"{self.base_url}catalog/list/view?key={api_key}",
headers=self.headers,
user_agent=self.user_agent,
) as response:
if response.status == 200:
return await response.json()
else:
raise Exception(f"Error fetching list, {response.status}")
@classmethod
async def catalog_lookup(self, set_id: int):
"""
Fetch details from the brickowl API for a specific LEGO set.
set_id: The LEGO set ID to look up.
returns JSON response containing set details.
"""
async with self.session.get(
f"{self.base_url}catalog/lookup/{set_id}view?key={api_key}",
headers=self.headers,
user_agent=self.user_agent,
) as response:
if response.status == 200:
return await response.json()
else:
raise Exception(f"Error fetching set {set_id}, {response.status}")
@classmethod
async def catalog_availability(self, set_id: int):
"""
Fetch availability information for a specific LEGO set.
"""
async with self.session.get(
f"{self.base_url}catalog/availability/{set_id}view?key={api_key}",
headers=self.headers,
user_agent=self.user_agent,
) as response:
if response.status == 200:
return await response.json()
else:
raise Exception(
f"Error fetching fetching availability for {set_id}, {response.status}"
)
@classmethod
async def catalog_id_lookup(self, set_id: int):
async with self.session.get(
f"{self.base_url}catalog/id_lookup/{set_id}view?key={api_key}",
headers=self.headers,
user_agent=self.user_agent,
) as response:
if response.status == 200:
return await response.json()
else:
raise Exception(f"ID lookup failed for set {set_id}, {response.status}")
@classmethod
async def catalog_bulk(self):
async with self.session.get(
f"{self.base_url}catalog/bulk/view?key={api_key}",
headers=self.headers,
user_agent=self.user_agent,
) as response:
if response.status == 200:
return await response.json()
else:
raise Exception(f"Error fetching bulk, {response.status}")
@classmethod
async def catalog_search(self, query: str):
async with self.session.get(
f"{self.base_url}catalog/search/{query}view?key={api_key}",
headers=self.headers,
user_agent=self.user_agent,
) as response:
if response.status == 200:
return await response.json()
else:
raise Exception(f"Search term {set_id} failed, {response.status}")
# Usage:
# obj = await ABrickowlAPI.catalog_lookup(value)
+137
View File
@@ -0,0 +1,137 @@
"""
abricksetapi.py - Asynchronous Brickset API Client
This module provides asynchronous interactions with the Brickset API.
"""
import asyncio
import os
import aiohttp
import json
import dotenv
from typing import Dict, Any
from asyncio_throttle.throttler import Throttler
dotenv.load_dotenv()
class AsyncBricksetAPI:
"""Asynchronous client for the Brickset API"""
def __init__(self, debug: bool = False):
self.username = os.getenv("BRICKSET_USERNAME")
self.password = os.getenv("BRICKSET_PASS")
self.api_key = os.getenv("BRICKSET_API_KEY")
self.debug = debug
self.headers = {"UserAgent": "Edderkop/0.1"}
self.throttler = Throttler(rate_limit=500, period=60)
self.user_hash = None
async def login(self) -> Dict[str, Any]:
"""Asynchronously authenticate with Brickset API"""
url = "https://brickset.com/api/v3.asmx/login"
params = {
"username": self.username,
"password": self.password,
"apiKey": self.api_key,
}
async with aiohttp.ClientSession() as session:
async with session.get(
url, params=params, headers=self.headers # type: ignore
) as response:
if self.debug:
print(f"[@DEBUG] Login response status: {response.status}")
data = await response.read()
result = json.loads(data.decode("utf-8"))
self.user_hash = result.get("hash")
return result
async def get_set(self, set_number: str) -> Dict[str, Any]:
"""
Fetch LEGO set information asynchronously
Args:
set_number (str): LEGO set number
Returns:
Dict containing set information
"""
if not self.user_hash:
await self.login()
url = "https://brickset.com/api/v3.asmx/getSets"
params = {
"apiKey": self.api_key,
"userHash": self.user_hash,
"params": json.dumps({"setNumber": f"{set_number}-1"}),
}
async with aiohttp.ClientSession() as session:
async with session.get(
url, params=params, headers=self.headers
) as response:
if self.debug:
print(f"[@DEBUG] Get set response status: {response.status}")
if response.status == 200:
data = await response.read()
result = json.loads(data.decode("utf-8"))
if not result.get("sets"):
raise ValueError(f"No data found for set {set_number}")
set_data = result["sets"][0]
our_set = {
"set_num": set_data["number"],
"set_name": set_data["name"],
"set_theme": set_data["theme"],
"set_image": set_data["image"]["thumbnailURL"],
"set_barcode": set_data["barcode"]["UPC"],
"set_retailprice": round(
set_data["LEGOCom"]["US"]["retailPrice"]
),
}
# Calculate adjusted price (10% markup)
retail_price = our_set["set_retailprice"]
our_set["set_adjustredprice"] = retail_price + (
(10 / 100) * retail_price
)
# Set PW inventory name with theme
our_set["pw_name"] = (
f"{our_set['set_num']} - {our_set['set_theme'].upper()} {our_set['set_name']}"
)
# Flag for manual price review if needed
if our_set["set_retailprice"] >= 110:
our_set["requires_manual_price"] = True
return our_set
elif response.status == 500:
raise Exception(
"Internal Server Error: Please check API key and parameters."
)
else:
raise Exception(f"Unexpected response: {response.status}")
async def throttled_get_set(self, set_number: str) -> Dict[str, Any]:
"""Get set information with rate limiting"""
async with self.throttler:
return await self.get_set(set_number)
async def main():
"""Example usage"""
client = AsyncBricksetAPI(debug=True)
try:
set_info = await client.throttled_get_set("10349")
print(json.dumps(set_info, indent=2))
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
asyncio.run(main())
+135
View File
@@ -0,0 +1,135 @@
import os
import dotenv
import json
import asyncio
import uuid
import re
import get_sq_env
def get_set_number(text: str) -> int:
# Remove all letters, keeping only digits and spaces
numbers = re.findall(r"\d{5}", text)
if not numbers:
raise ValueError("No five-digit number found in string")
return int(numbers[0])
SQUARE_TOKEN = get_sq_env.SQUARE_TOKEN
LEGO_CAT_ID = get_sq_env.LEGO_CAT_ID
ENVIRONMENT = get_sq_env.environment
from square import AsyncSquare
from square.environment import SquareEnvironment
client = AsyncSquare(token=SQUARE_TOKEN, environment=ENVIRONMENT) # type: ignore
async def get_itm(sku: str):
"""
Asynchronously retrieves item details from Square Catalog using a SKU.
Args:
sku (str): The SKU or text filter to search for the item.
Returns:
dict: A dictionary containing the item's name and ID.
Raises:
IndexError: If no items are found matching the SKU.
Exception: If there is an error communicating with the Square API.
"""
result = await client.catalog.search_items(text_filter=sku, limit=1)
result_json = json.loads(result.model_dump_json())
name = result_json["items"][0]["item_data"]["name"]
itm_id = result_json["items"][0]["id"]
return {"name": name, "id": itm_id}
async def update_itm(lego_set: dict):
"""
Asynchronously updates an item in the Square catalog based on the provided LEGO set information.
Args:
lego_set (dict): A dictionary containing LEGO set details. Expected keys include:
- "sets": List of set dictionaries, each with "name", "number", "barcode" (with "UPC"), and "image" (with "thumbnailURL").
- "pw_name": The name associated with the LEGO set in another system.
- "set_adjustredprice": The adjusted price for the set.
Returns:
bool: True if the update operation was successful.
Raises:
KeyError: If expected keys are missing from the input dictionary.
Exception: For errors during Square API interaction.
"""
set_name = lego_set["sets"][0]["name"]
set_number = lego_set["sets"][0]["number"]
set_upc = lego_set["sets"][0]["barcode"]["UPC"]
set_image = lego_set["sets"][0]["image"]["thumbnailURL"]
pw_name = lego_set["pw_name"]
set_adjustedprice = lego_set["set_adjustredprice"]
result = await client.catalog.search_items(text_filter=set_number, limit=1)
itm_id = json.loads(result.model_dump_json())["items"][0]["id"]
# await sq.catalog.object.upsert()
return True
async def acat2id(cat_id: str):
"""
Return a list of dictionaries containing item IDs and names within the specified category ID.
Args:
cat_id (str): The category ID to search for items.
Returns:
list[dict]: A list of dictionaries, each containing:
- 'id': The item's ID
- 'name': The item's name
"""
result = await client.catalog.search(
object_types=["ITEM"],
query={
"exact_query": {
"attribute_name": "category_id",
"attribute_value": cat_id,
}
},
)
result_json = json.loads(result.model_dump_json())
items = [
{
"id": obj["id"],
"name": obj.get("item_data", {}).get("name", "Unknown"),
"number": get_set_number(obj.get("item_data", {}).get("name", ""))
}
for obj in result_json.get("objects", [])
]
return items
async def test() -> None:
task = asyncio.create_task(acat2id(LEGO_CAT_ID)) # type: ignore
await asyncio.sleep(1) # Give the task some time to complete
result = await task
print("OBJECTS IN LEGO CATEGORY:")
for item in result:
print(f"ID: {item['id']}, Name: {item['name']}")
# eturn
# pass
"""
result = await sq.catalog.search_items(text_filter="71433", limit=1)
result_json = json.loads(result.model_dump_json())
name = result_json["items"][0]["item_data"]["name"]
print(f"Item name: {name}")"""
if __name__ == "__main__":
print(f"@[DEBUG] ENV:\t{ENVIRONMENT}")
print(f"@[DEBUG] CATID:\t{LEGO_CAT_ID}")
asyncio.run(test())
+20
View File
@@ -0,0 +1,20 @@
import subprocess
def generate(text, font="smblock", filter_effect=None, colors=None):
command = ["toilet", "-f", font]
if filter_effect:
command.extend(["-F", filter_effect])
if colors:
# Toilet uses a specific format for color arguments, e.g., --gay or --metal
# For general colors, you might need to use specific filters or direct color codes.
# This example focuses on filters that inherently use colors.
pass # More complex color handling would be needed for arbitrary colors
command.append(text)
try:
result = subprocess.run(command, capture_output=True, text=True, check=True)
return result.stdout
except subprocess.CalledProcessError as e:
return f"Error executing toilet: {e}\n{e.stderr}"
+60
View File
@@ -0,0 +1,60 @@
import os
import requests
import json
import dotenv
dotenv.load_dotenv()
USERNAME = os.getenv("BRICKSET_USERNAME")
PASS = os.getenv("BRICKSET_PASS")
BRICKSET_API_KEY = os.getenv("BRICKSET_API_KEY")
debug = False # Set to True to enable debug output
def login(username, password):
url = "https://brickset.com/api/v3.asmx/login"
headers = {"UserAgent": "Edderkop/0.1"}
params = {
"UserAgent": "Edderkop/0.1",
"username": username,
"password": password,
"apiKey": BRICKSET_API_KEY,
}
response = requests.get(url, params=params, headers=headers)
if debug:
print(f"[@DEBUG] Login response: {response.status_code} - {response.text}\n")
return response.json()
def get_set(set_number: int) -> dict:
login_response = login(USERNAME, PASS)
url = f"https://brickset.com/api/v3.asmx/getSets?apiKey={BRICKSET_API_KEY}&userHash={login_response["hash"]}&params={{'setNumber' : '{set_number}-1'}}"
response = requests.get(url)
if response.status_code == 200:
data = response.json()
our_set = {
"set_num": data["sets"][0]["number"],
"set_name": data["sets"][0]["name"],
"set_theme": data["sets"][0]["theme"],
"set_image": data["sets"][0]["image"]["thumbnailURL"],
"set_barcode": data["sets"][0]["barcode"]["UPC"],
"set_retailprice": round(data["sets"][0]["LEGOCom"]["US"]["retailPrice"]),
}
# add PW 10% b4 tax
our_set.update(
{"set_adjustredprice": our_set + ((10 / 100) * our_set["set_retailprice"])}
)
our_set["pw_name"] = (
f"{our_set['set_num']} - {our_set["set_theme"].upper()} {our_set['set_name']}"
)
if our_set["set_retailprice"] >= 110:
our_set["requires_manual_price"] = True
return our_set
elif response.status_code == 500:
raise Exception(
"Internal Server Error: Please check the API key and parameters."
)
else:
raise Exception("Unexpected response from the API.")
+221
View File
@@ -0,0 +1,221 @@
import aiosqlite
import logging
from typing import Optional, List, Dict, Any
import json
import os
logger = logging.getLogger(__name__)
# Get the absolute path to edderkop.db in the project root
DEFAULT_DB_PATH = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "edderkop.db"
)
if not os.path.exists(DEFAULT_DB_PATH):
raise FileNotFoundError(f"Database file not found at {DEFAULT_DB_PATH}")
print('Please run "src/scripts/EDDERKOP.SQL" to setup the database.')
os.exit(0)
class AsyncSchedulerDB:
"""
Asynchronous SQLite database manager for APScheduler.
Handles job storage, retrieval, and database operations using the main edderkop.db database.
"""
def __init__(self, db_path: str = DEFAULT_DB_PATH):
"""
Initialize the database manager.
Args:
db_path (str, optional): Path to the SQLite database file.
Defaults to edderkop.db in project root.
"""
self.db_path = db_path
self._initialized = False
logger.info(f"Using database at: {self.db_path}")
async def initialize(self):
"""Initialize the database schema if it doesn't exist."""
if self._initialized:
return
async with aiosqlite.connect(self.db_path) as db:
# Create apscheduler jobs table if it doesn't exist
await db.execute(
"""
CREATE TABLE IF NOT EXISTS apscheduler_jobs (
id VARCHAR(191) NOT NULL,
next_run_time FLOAT,
job_state BLOB NOT NULL,
PRIMARY KEY (id)
)
"""
)
await db.commit()
self._initialized = True
logger.info(f"Initialized scheduler database at {self.db_path}")
async def get_all_jobs(self) -> List[Dict[str, Any]]:
"""
Retrieve all jobs from the database.
Returns:
List[Dict[str, Any]]: List of job dictionaries
"""
await self.initialize()
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM apscheduler_jobs ORDER BY next_run_time"
) as cursor:
rows = await cursor.fetchall()
return [
{
"id": row["id"],
"next_run_time": row["next_run_time"],
"job_state": json.loads(row["job_state"]),
}
for row in rows
]
async def get_job(self, job_id: str) -> Optional[Dict[str, Any]]:
"""
Retrieve a specific job by ID.
Args:
job_id (str): The ID of the job to retrieve
Returns:
Optional[Dict[str, Any]]: Job dictionary if found, None otherwise
"""
await self.initialize()
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM apscheduler_jobs WHERE id = ?", (job_id,)
) as cursor:
row = await cursor.fetchone()
if row:
return {
"id": row["id"],
"next_run_time": row["next_run_time"],
"job_state": json.loads(row["job_state"]),
}
return None
async def add_job(
self, job_id: str, next_run_time: float, job_state: Dict[str, Any]
) -> bool:
"""
Add a new job to the database.
Args:
job_id (str): Unique identifier for the job
next_run_time (float): Unix timestamp for next scheduled run
job_state (Dict[str, Any]): Job state dictionary
Returns:
bool: True if successful, False otherwise
"""
await self.initialize()
try:
async with aiosqlite.connect(self.db_path) as db:
await db.execute(
"INSERT INTO apscheduler_jobs (id, next_run_time, job_state) VALUES (?, ?, ?)",
(job_id, next_run_time, json.dumps(job_state)),
)
await db.commit()
return True
except aiosqlite.IntegrityError:
logger.error(f"Job with ID {job_id} already exists")
return False
except Exception as e:
logger.error(f"Error adding job: {e}")
return False
async def update_job(
self, job_id: str, next_run_time: float, job_state: Dict[str, Any]
) -> bool:
"""
Update an existing job in the database.
Args:
job_id (str): ID of the job to update
next_run_time (float): New next run time
job_state (Dict[str, Any]): Updated job state
Returns:
bool: True if successful, False if job not found
"""
await self.initialize()
async with aiosqlite.connect(self.db_path) as db:
cursor = await db.execute(
"""UPDATE apscheduler_jobs
SET next_run_time = ?, job_state = ?
WHERE id = ?""",
(next_run_time, json.dumps(job_state), job_id),
)
await db.commit()
return cursor.rowcount > 0
async def remove_job(self, job_id: str) -> bool:
"""
Remove a job from the database.
Args:
job_id (str): ID of the job to remove
Returns:
bool: True if job was removed, False if not found
"""
await self.initialize()
async with aiosqlite.connect(self.db_path) as db:
cursor = await db.execute(
"DELETE FROM apscheduler_jobs WHERE id = ?", (job_id,)
)
await db.commit()
return cursor.rowcount > 0
async def remove_all_jobs(self) -> int:
"""
Remove all jobs from the database.
Returns:
int: Number of jobs removed
"""
await self.initialize()
async with aiosqlite.connect(self.db_path) as db:
cursor = await db.execute("DELETE FROM apscheduler_jobs")
await db.commit()
return cursor.rowcount
async def get_due_jobs(self, now: float) -> List[Dict[str, Any]]:
"""
Get all jobs that are due to run.
Args:
now (float): Current timestamp
Returns:
List[Dict[str, Any]]: List of due jobs
"""
await self.initialize()
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"""SELECT * FROM apscheduler_jobs
WHERE next_run_time <= ?
ORDER BY next_run_time""",
(now,),
) as cursor:
rows = await cursor.fetchall()
return [
{
"id": row["id"],
"next_run_time": row["next_run_time"],
"job_state": json.loads(row["job_state"]),
}
for row in rows
]
+116
View File
@@ -0,0 +1,116 @@
import os
import requests
import json
import dotenv
from typing import Dict, Any
dotenv.load_dotenv()
class BricksetAPI:
"""Class for interacting with the Brickset API"""
def __init__(self, debug: bool = False):
self.username = os.getenv("BRICKSET_USERNAME")
self.password = os.getenv("BRICKSET_PASS")
self.api_key = os.getenv("BRICKSET_API_KEY")
self.debug = debug
self.headers = {"UserAgent": "Edderkop/0.1"}
self.user_hash = None
def login(self) -> Dict[str, Any]:
"""Login to Brickset API and get user hash"""
url = "https://brickset.com/api/v3.asmx/login"
params = {
"username": self.username,
"password": self.password,
"apiKey": self.api_key,
}
response = requests.get(url, params=params, headers=self.headers)
if self.debug:
print(f"[@DEBUG] Login response: {response.status_code}")
print(f"[@DEBUG] Response content: {response.text}")
try:
data = response.json()
self.user_hash = data.get("hash")
if not self.user_hash:
raise ValueError("No hash received in login response")
return data
except json.JSONDecodeError as e:
print(f"[@ERROR] Failed to decode JSON response: {e}")
print(f"[@DEBUG] Raw response: {response.text}")
raise
def get_set(self, set_number: str) -> Dict[str, Any]:
"""Get LEGO set information by set number"""
if not self.user_hash:
self.login()
url = "https://brickset.com/api/v3.asmx/getSets"
params = {
"apiKey": self.api_key,
"userHash": self.user_hash,
"params": json.dumps({"setNumber": f"{set_number}-1"}),
}
response = requests.get(url, params=params, headers=self.headers)
if self.debug:
print(f"[@DEBUG] Get set response: {response.status_code}")
print(f"[@DEBUG] Response content: {response.text}")
if response.status_code == 200:
try:
data = response.json()
if not data.get("sets"):
raise ValueError(f"No set data found for {set_number}")
set_data = data["sets"][0]
our_set = {
"set_num": set_data["number"],
"set_name": set_data["name"],
"set_image": set_data["image"]["thumbnailURL"],
"set_barcode": set_data["barcode"]["UPC"],
"set_retailprice": round(set_data["LEGOCom"]["US"]["retailPrice"]),
}
# Calculate adjusted price (10% markup)
retail_price = our_set["set_retailprice"]
our_set["set_adjustredprice"] = retail_price + (
(10 / 100) * retail_price
)
# Set Playwell inventory name
if our_set["theme"] == "Duplo":
our_set["pw_name"] = (
f"{our_set['set_num']} - DUPLO {our_set['set_name']}"
)
else:
our_set["pw_name"] = f"{our_set['set_num']} - {our_set['set_name']}"
return our_set
except json.JSONDecodeError as e:
# print(f"[@ERROR] Failed to decode JSON response: {e}")
# print(f"[@DEBUG] Raw response: {response.text}")
raise
except KeyError as e:
# print(f"[@ERROR] Missing expected field in response: {e}")
# print(f"[@DEBUG] Response data: {data}")
raise
elif response.status_code == 500:
raise Exception(
"Internal Server Error: Please check API key and parameters."
)
else:
raise Exception(f"Unexpected response: {response.status_code}")
def example():
client = BricksetAPI(debug=True)
try:
set_info = client.get_set("10931")
print(set_info)
except Exception as e:
print(f"Error: {e}")
+16
View File
@@ -0,0 +1,16 @@
import os
import dotenv
from square.environment import SquareEnvironment
dotenv.load_dotenv()
LEGO_CAT_ID = os.getenv("SQUARE_LEGO_CAT_ID")
if os.getenv("SQUARE_ENV") == "production":
SQUARE_APP_ID = os.getenv("PROD_SQUARE_APP_ID")
SQUARE_TOKEN = os.getenv("PROD_SQUARE_TOKEN")
environment = SquareEnvironment.PRODUCTION
else:
SQUARE_APP_ID = os.getenv("SANDBOX_SQUARE_APP_ID")
SQUARE_TOKEN = os.getenv("SANDBOX_SQUARE_TOKEN")
environment = SquareEnvironment.SANDBOX
+66
View File
@@ -0,0 +1,66 @@
import json
import get_sq_env
SQUARE_TOKEN = get_sq_env.SQUARE_TOKEN
LEGO_CAT_ID = get_sq_env.LEGO_CAT_ID
ENVIRONMENT = get_sq_env.environment
from square import Square
from square.environment import SquareEnvironment
def get_square_client():
"""Initialize and return a Square client."""
if not SQUARE_TOKEN:
raise ValueError("SQUARE_TOKEN environment variable is not set.")
square = Square(token=SQUARE_TOKEN, environment=ENVIRONMENT) # type: ignore
return square
client = get_square_client()
def get_itm(item_id: str):
"""Fetch an item from Square by its ID."""
result = client.catalog.search_items(text_filter=item_id, limit=1)
result_json = json.loads(result.model_dump_json())
ids = {
"item_id": result_json["items"][0]["id"],
"vari_id": result_json["items"][0]["item_data"]["variations"][0][
"item_variation_data"
]["item_id"],
}
item = {
"set_name": result_json["items"][0]["item_data"]["name"],
"set_price": result_json["items"][0]["item_data"]["variations"][0][
"item_variation_data"
]["price_money"],
"set_barcode": result_json["items"][0]["item_data"]["variations"][0][
"item_variation_data"
]["sku"],
}
return [ids, item]
def cat2id(cat_id: str):
"""Return a list of item IDs within the specified category ID."""
result = client.catalog.search(
object_types=["ITEM"],
query={
"exact_query": {
"attribute_name": "category_id",
"attribute_value": cat_id,
}
},
)
result_json = json.loads(result.model_dump_json())
item_ids = [obj["id"] for obj in result_json.get("objects", [])]
return item_ids
def test() -> None:
# all_lego_sets = cat2id(SQUARE_LEGO_CAT_ID) # type: ignore
# result = get_itm("71433")
# print(result)
pass
+7
View File
@@ -0,0 +1,7 @@
CREATE TABLE "REVIEW" (
"sq_object" TEXT NOT NULL UNIQUE,
"time_added" INTEGER,
"set_number" INTEGER,
"time_modified" INTEGER,
PRIMARY KEY("sq_object")
)
+113
View File
@@ -0,0 +1,113 @@
import asyncio
import aiohttp.web
import uuid
import os
from base64 import b64decode
import dotenv
dotenv.load_dotenv()
SQUARE_LEGO_CAT_ID = os.environ.get("SQUARE_LEGO_CAT_ID")
from asyncio_throttle.throttler import Throttler
import asquare as sq
import abricksetapi as alego
def valid_uuid(val):
try:
uuid.UUID(str(val))
return True
except ValueError:
return False
async def fullsync():
"""'
full_db = await sq.acat2id(SQUARE_LEGO_CAT_ID)
for l_set in full_db:
l_setinfo = await alego.throttled_get_set(l_set)
await sq.update_itm(l_set, l_setinfo)"""
async def stop():
pass
async def priceonly():
pass
async def handle_post(request):
valid_cmd = ["RUNFULLSYNC", "STOP", "RUNPRICEONLY"]
async def handle_cmd(cmd: str):
if cmd not in valid_cmd:
return False
else:
return True
idp_key = request.headers.get("Idempotency-Key")
user_agent = request.headers.get("User-Agent")
if not idp_key and not valid_uuid(idp_key) and user_agent != "Kopling/0.1":
# 412 Precondition Failed
# The server does not meet one of the preconditions that the requester put on the request header fields.
return aiohttp.web.Response(status=412, text="412 Headers rejected!")
else:
data = await request.post()
reqdata = data.get(b64decode("CMD"))
if await handle_cmd(reqdata):
match reqdata:
case "RUNFULLSYNC":
print("Running full sync...")
await fullsync()
return True
case "STOP":
await stop()
return True
case "RUNPRICEONLY":
await priceonly()
return True
else:
return aiohttp.web.Response(status=400, text="400 Bad Request.")
result = {"message": f"Edderkop/0.1 received command: {reqdata}"}
return aiohttp.web.json_response(status=200, data=result)
app = aiohttp.web.Application()
app.router.add_post("/", handle_post)
import subprocess
def generate_toilet_banner(text, font="smblock", filter_effect=None, colors=None):
command = ["toilet", "-f", font]
if filter_effect:
command.extend(["-F", filter_effect])
if colors:
# Toilet uses a specific format for color arguments, e.g., --gay or --metal
# For general colors, you might need to use specific filters or direct color codes.
# This example focuses on filters that inherently use colors.
pass # More complex color handling would be needed for arbitrary colors
command.append(text)
try:
result = subprocess.run(command, capture_output=True, text=True, check=True)
return result.stdout
except subprocess.CalledProcessError as e:
return f"Error executing toilet: {e}\n{e.stderr}"
async def main():
throttler = Throttler(rate_limit=500, period=60) # 500 requests per minute
if __name__ == "__main__":
print(generate_toilet_banner("Edderkop v0.1"))
print("Built for PLAY-WELL TEKNOLOGIES by contact@traps.moe\n\n")
aiohttp.web.run_app(app, port=7432)
+6
View File
@@ -0,0 +1,6 @@
import src.libsquare as sq
import src.bricksetapi as lego
oursq = sq.get_itm("71417")
ourlego = lego.get_set(71417)
print(f"{oursq}\n\n{ourlego}\n\n")
+39
View File
@@ -0,0 +1,39 @@
import asyncio
import aiohttp
from uuid import uuid4
from base64 import b64encode
import os
async def idp_request(cmd: str):
headers = {
"Idempotency-Key": f"{uuid4()}",
"Content-Type": "application/json",
"User-Agent": "Kopling/0.12",
}
print(f"[@DEBUG] Idempotency-Key: {headers['Idempotency-Key']}")
enc_cmd = b64encode(f"{cmd}".encode("utf-8")).decode("utf-8")
print("[@DEBUG] Encoded command:", enc_cmd)
print("[@DEBUG] Sending request..")
try:
async with aiohttp.ClientSession() as session:
async with session.post(
"http://localhost:7432", # Added http:// protocol
headers=headers,
json={"CMD": enc_cmd}, # Changed to json instead of data
) as resp:
print(f"[@DEBUG] Response status: {resp.status}")
print(f"[@DEBUG] Response body: {await resp.text()}")
except Exception as e:
print(f"\nRequest failed: {e}")
os._exit(-1)
async def main():
# Await the task instead of just creating it
await idp_request("RUNUPDATE")
if __name__ == "__main__":
asyncio.run(main())
+121
View File
@@ -0,0 +1,121 @@
import asyncio
import json
from aiohttp import web
from base64 import b64decode
from typing import Dict, Any
import uuid
from time import time
import dotenv
import os
import re
import logging
from pathlib import Path
Path("./logs/").mkdir(parents=True, exist_ok=True)
import banner
import get_sq_env
import asquare as sq
from abricksetapi import AsyncBricksetAPI
lego = AsyncBricksetAPI(debug=True)
dotenv.load_dotenv()
SQUARE_LEGO_CAT_ID = os.environ.get("SQUARE_LEGO_CAT_ID")
from asyncio_throttle.throttler import Throttler
logger = logging.getLogger("aiohttp.access")
logging.basicConfig(
filename="./logs/edderkop.log", encoding="utf-8", level=logging.DEBUG
)
UA = re.compile(r"^kopling\/?", re.IGNORECASE)
async def run_update():
set_list = await sq.acat2id(SQUARE_LEGO_CAT_ID)
for l_set in set_list:
print(f"[@DEBUG]{l_set}")
l_setinfo = await lego.throttled_get_set(l_set["number"])
print(f"[@DEBUG]{l_setinfo}")
# await sq.update_itm(l_set, l_setinfo)
def valid_uuid(val):
try:
uuid.UUID(str(val))
return True
except ValueError:
return False
async def request_handler(request: web.Request) -> web.Response:
"""Handle incoming POST requests with command processing"""
try:
# Verify request method
if request.method != "POST":
logger.error(
f"{time()}\tInvalid request method from %s at ", request.remote
)
return web.Response(status=405, text="Method not allowed. Use POST.")
# Get JSON data from request
try:
data = await request.json()
except json.JSONDecodeError:
logger.error("Invalid JSON from %s", request.remote)
return web.Response(
status=400, text="{time()}\tInvalid JSON in request body"
)
# Validate Idempotency-Key header
if valid_uuid(request.headers.get("Idempotency-Key")) is False:
logger.error("{time()}\tInvalid Idempotency-Key from %s", request.remote)
return web.Response(status=412, text="412 Headers rejected!")
# Validate User-Agent header
if not re.match(UA, str(request.headers.get("User-Agent"))):
logger.error("{time()}\tInvalid User-Agent from %s", request.remote)
return web.Response(status=412, text="412 Headers rejected!")
# Validate command exists
if "CMD" not in data:
logger.error("{time()}\tMissing CMD field from %s", request.remote)
return web.Response(status=400, text="Missing CMD field in request")
# Decode and process command
try:
decoded_cmd = b64decode(data["CMD"]).decode("utf-8")
print(
f"""At: {time()}\nReceived command: {decoded_cmd}\nUser-Agent: {request.headers.get('User-Agent')}\nIdempotency-Key: {request.headers.get('Idempotency-Key')}"""
)
logging.info(
f"{time()}\tCommand: {decoded_cmd}\tFrom: {request.remote}\tUA: {request.headers.get('User-Agent')}\tIdempotency-Key: {request.headers.get('Idempotency-Key')}"
)
# Process the command
if decoded_cmd == "RUNUPDATE":
# Add your command handling logic here
await run_update()
response_data = {"status": "success", "message": "Update started"}
logging.info(f"{time()}\tUpdate command processed successfully")
return web.json_response(response_data)
else:
return web.Response(status=400, text=f"Unknown command: {decoded_cmd}")
except Exception as e:
print(f"Error processing command: {str(e)}")
return web.Response(status=500, text=f"Server error: {str(e)}")
except Exception as e:
print(f"Request handler error: {str(e)}")
return web.Response(status=500, text="Internal server error")
app = web.Application()
app.router.add_post("/", request_handler)
if __name__ == "__main__":
print(banner.generate("Edderkop v0.1"))
print("Built for PLAY-WELL TEKNOLOGIES by contact@traps.moe\n\n")
web.run_app(app, host="localhost", port=7432)
+15
View File
@@ -0,0 +1,15 @@
import aiohttp
import asyncio
from asyncio_throttle.throttler import Throttler
throttler = Throttler(rate_limit=500, period=60) # 500 requests per minute
async def worker(throttler, session):
while True:
async with throttler:
async with session.get("http://example.com") as resp:
do_some_job_with(await resp.text())
await asyncio.sleep(0.05)
+62
View File
@@ -0,0 +1,62 @@
import pytest
import asyncio
from unittest.mock import patch, AsyncMock
from ..src.abricksetapi import get_set
@pytest.mark.asyncio
@patch("abricksetapi.login", new_callable=AsyncMock)
@patch("abricksetapi.aiohttp.ClientSession")
async def test_get_set_success(mock_session_cls, mock_login):
# Mock login response
mock_login.return_value = {"hash": "fakehash"}
# Prepare mock response data
mock_response_data = {
"sets": [
{
"number": "10349-1",
"name": "Icons: Modular Jazz Club",
"image": {"thumbnailURL": "http://image.url"},
"barcode": {"UPC": "123456789012"},
"LEGOCom": {"US": {"retailPrice": 229.99}},
}
]
}
mock_response = AsyncMock()
mock_response.status = 200
mock_response.read.return_value = asyncio.Future()
mock_response.read.return_value.set_result(
str.encode(str(mock_response_data).replace("'", '"'))
)
# Mock session.get to return our mock_response
mock_session = AsyncMock()
mock_session.get.return_value.__aenter__.return_value = mock_response
mock_session_cls.return_value.__aenter__.return_value = mock_session
result = await get_set("10349")
assert result["set_num"] == "10349-1"
assert result["set_name"] == "Icons: Modular Jazz Club"
assert result["set_image"] == "http://image.url"
assert result["set_barcode"] == "123456789012"
assert result["set_retailprice"] == 230
assert "set_adjustredprice" in result
@pytest.mark.asyncio
@patch("abricksetapi.login", new_callable=AsyncMock)
@patch("abricksetapi.aiohttp.ClientSession")
async def test_get_set_api_error(mock_session_cls, mock_login):
mock_login.return_value = {"hash": "fakehash"}
mock_response = AsyncMock()
mock_response.status = 500
mock_response.read.return_value = asyncio.Future()
mock_response.read.return_value.set_result(b"{}")
mock_session = AsyncMock()
mock_session.get.return_value.__aenter__.return_value = mock_response
mock_session_cls.return_value.__aenter__.return_value = mock_session
with pytest.raises(Exception) as excinfo:
await get_set("badset")
assert "Internal Server Error" in str(excinfo.value)
+33
View File
@@ -0,0 +1,33 @@
import pytest
import asyncio
from unittest.mock import AsyncMock, patch
from asquare import acat2id
@pytest.mark.asyncio
async def test_acat2id_returns_item_ids():
mock_result = AsyncMock()
mock_result.model_dump_json.return_value = (
'{"objects": [{"id": "item1"}, {"id": "item2"}]}'
)
with patch("asquare.client.catalog.search", return_value=mock_result):
item_ids = await acat2id("test_cat_id")
assert item_ids == ["item1", "item2"]
@pytest.mark.asyncio
async def test_acat2id_empty_objects():
mock_result = AsyncMock()
mock_result.model_dump_json.return_value = '{"objects": []}'
with patch("asquare.client.catalog.search", return_value=mock_result):
item_ids = await acat2id("empty_cat_id")
assert item_ids == []
@pytest.mark.asyncio
async def test_acat2id_no_objects_key():
mock_result = AsyncMock()
mock_result.model_dump_json.return_value = "{}"
with patch("asquare.client.catalog.search", return_value=mock_result):
item_ids = await acat2id("no_objects_cat_id")
assert item_ids == []