diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ee5b521a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/dist/ +/.idea/ diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..aea95a5d --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pyinstaller = "*" +black = "*" + +[packages] +requests = "*" +click = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..b2fa440a --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,111 @@ +{ + "_meta": { + "hash": { + "sha256": "f10785a364494f58033c00c9ae85cae0b89bec04840e99607d399979516cbef3" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "index": "pypi", + "version": "==7.1.2" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "requests": { + "hashes": [ + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + ], + "index": "pypi", + "version": "==2.24.0" + }, + "urllib3": { + "hashes": [ + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.10" + } + }, + "develop": { + "altgraph": { + "hashes": [ + "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa", + "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe" + ], + "version": "==0.17" + }, + "future": { + "hashes": [ + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.18.2" + }, + "pefile": { + "hashes": [ + "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645" + ], + "markers": "sys_platform == 'win32'", + "version": "==2019.4.18" + }, + "pyinstaller": { + "hashes": [ + "sha256:970beb07115761d5e4ec317c1351b712fd90ae7f23994db914c633281f99bab0" + ], + "index": "pypi", + "version": "==4.0" + }, + "pyinstaller-hooks-contrib": { + "hashes": [ + "sha256:4935b2b48c2a38ab4f8f80d77147781642bff7d31748fca07d26dbdeeefb560d", + "sha256:71982f32883309e89675bfd10093cdcaca4f894bce11a91a34559b8224cf5e63" + ], + "version": "==2020.8" + }, + "pywin32-ctypes": { + "hashes": [ + "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", + "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.2.0" + } + } +} diff --git a/app.py b/app.py new file mode 100644 index 00000000..4540fb94 --- /dev/null +++ b/app.py @@ -0,0 +1,4 @@ +from cli import cli + +if __name__ == "__main__": + cli.main() diff --git a/app.spec b/app.spec new file mode 100644 index 00000000..695b6d05 --- /dev/null +++ b/app.spec @@ -0,0 +1,33 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + + +a = Analysis(['app.py'], + pathex=['C:\\Users\\hari\\PycharmProjects\\nvidia-bot'], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='app', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True ) diff --git a/cli/__init__.py b/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cli/cli.py b/cli/cli.py new file mode 100644 index 00000000..0b2bb2f4 --- /dev/null +++ b/cli/cli.py @@ -0,0 +1,19 @@ +import click + +from cli.gpu import GPU +from stores.nvidia import NvidiaBuyer + + +@click.group() +def main(): + pass + + +@click.command() +@click.argument("gpu", type=GPU()) +def buy(gpu): + nv = NvidiaBuyer() + nv.buy(gpu) + + +main.add_command(buy) diff --git a/cli/gpu.py b/cli/gpu.py new file mode 100644 index 00000000..6a49e313 --- /dev/null +++ b/cli/gpu.py @@ -0,0 +1,17 @@ +import click + +from stores.nvidia import GPU_DISPLAY_NAMES + + +class GPU(click.ParamType): + name = "api-key" + + def convert(self, value, param, ctx): + if value.upper() not in GPU_DISPLAY_NAMES.keys(): + self.fail( + f"{value} is not a valid GPU, valid GPUs are {list(GPU_DISPLAY_NAMES.keys())}", + param, + ctx, + ) + + return value.upper() diff --git a/stores/__init__.py b/stores/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/stores/nvidia.py b/stores/nvidia.py new file mode 100644 index 00000000..59ba6b95 --- /dev/null +++ b/stores/nvidia.py @@ -0,0 +1,83 @@ +import logging +import webbrowser + +import requests + +log = logging.getLogger(__name__) +formatter = logging.Formatter( + "%(asctime)s : %(message)s : %(levelname)s -%(name)s", datefmt="%d%m%Y %I:%M:%S %p" +) +handler = logging.StreamHandler() +handler.setFormatter(formatter) +log.setLevel(10) +log.addHandler(handler) + +DIGITAL_RIVER_OUT_OF_STOCK_MESSAGE = "PRODUCT_INVENTORY_OUT_OF_STOCK" +DIGITAL_RIVER_API_KEY = "9485fa7b159e42edb08a83bde0d83dia" +DIGITAL_RIVER_PRODUCT_LIST_URL = "https://api.digitalriver.com/v1/shoppers/me/products" +DIGITAL_RIVER_STOCK_CHECK_URL = "https://api.digitalriver.com/v1/shoppers/me/products/{product_id}/inventory-status?" + +NVIDIA_CART_URL = "https://store.nvidia.com/store/nvidia/en_US/buy/productID.{product_id}/clearCart.yes/nextPage.QuickBuyCartPage" + +GPU_DISPLAY_NAMES = { + "2060S": "NVIDIA GEFORCE RTX 2060 SUPER", + "3080": "NVIDIA GEFORCE RTX 3080", + "3090": "NVIDIA GEFORCE RTX 3090", +} + + +def add_to_cart(product_id): + log.info(f"Adding {product_id} to cart!") + webbrowser.open_new(NVIDIA_CART_URL.format(product_id=product_id)) + + +def is_in_stock(product_id): + payload = { + "apiKey": DIGITAL_RIVER_API_KEY, + } + + url = DIGITAL_RIVER_STOCK_CHECK_URL.format(product_id=product_id) + + log.debug(f"Calling {url}") + response = requests.get(url, headers={"Accept": "application/json"}, params=payload) + log.debug(f"Returned {response.status_code}") + response_json = response.json() + product_status_message = response_json["inventoryStatus"]["status"] + log.info(f"Stock status is {product_status_message}") + return product_status_message != DIGITAL_RIVER_OUT_OF_STOCK_MESSAGE + + +class NvidiaBuyer: + def __init__(self): + self.product_data = {} + self.get_product_ids() + print(self.product_data) + + def get_product_ids(self, url=DIGITAL_RIVER_PRODUCT_LIST_URL): + log.debug(f"Calling {url}") + payload = { + "apiKey": DIGITAL_RIVER_API_KEY, + "expand": "product", + "fields": "product.id,product.displayName,product.pricing", + } + response = requests.get( + url, headers={"Accept": "application/json"}, params=payload + ) + + log.debug(response.status_code) + response_json = response.json() + for product_obj in response_json["products"]["product"]: + if product_obj["displayName"] in GPU_DISPLAY_NAMES.values(): + self.product_data[product_obj["displayName"]] = { + "id": product_obj["id"], + "price": product_obj["pricing"]["formattedListPrice"], + } + if response_json["products"].get("nextPage"): + self.get_product_ids(url=response_json["products"]["nextPage"]["uri"]) + + def buy(self, gpu): + product_id = self.product_data.get(GPU_DISPLAY_NAMES[gpu])["id"] + log.info(f"Checking stock for {GPU_DISPLAY_NAMES[gpu]}...") + while not is_in_stock(product_id): + sleep(5) + add_to_cart(product_id)