import all
This commit is contained in:
commit
881baaa1ed
25 files changed
+1779
No files matched your search
+25
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Edderkop
|
||||
|
||||
environment: poetry
|
||||
code style: black
|
||||
|
||||
built with
|
||||
|
||||
- aiohttp
|
||||
- squareup
|
||||
- apscheduler
|
||||
- aiosqlite
|
||||
@@ -0,0 +1,4 @@
|
||||
- glue shit together
|
||||
- test sq
|
||||
- test inserting brickowl into sq
|
||||
- test kopling
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../kopling"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
+14
@@ -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.")
|
||||
@@ -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"
|
||||
@@ -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
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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())
|
||||
@@ -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}"
|
||||
@@ -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"]}¶ms={{'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.")
|
||||
@@ -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
|
||||
]
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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 == []
|
||||
Reference in new issue
Block a user