Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
Empty file removed .env
Empty file.
55 changes: 55 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# === Python build artifacts ===
*.pyc
*.pyo
*.pyd
__pycache__/
*.log

# === SQLite & output files ===
*.sqlite3
*.db
output.json

# === Environment variables ===
.env
.env.*
*.env

# === Virtual environments ===
venv/
.venv/
.env/

# === VSCode project settings ===
.vscode/

# === macOS system files ===
*.DS_Store
*.egg-info/

# === Pytest and test cache ===
htmlcov/
.coverage
.cache/
pytest_cache/
.tox/

# === Jupyter Notebook ===
.ipynb_checkpoints/

# === Django migration artifacts (optional to ignore) ===
# Uncomment the lines below if you want to regenerate migrations often
# **/migrations/*.py
# **/migrations/*.pyc
# !**/migrations/__init__.py

# === FastAPI-specific artifacts ===
fastapi_email/email_db.sqlite3

# === IDE-specific ===
.idea/
*.sublime-project
*.sublime-workspace

# === GitHub Codespaces or devcontainers ===
.devcontainer/
14 changes: 14 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"python.analysis.extraPaths": [
"./webscraper/ABC"
],
"python.testing.unittestArgs": [
"-v",
"-s",
"./webscraper",
"-p",
"*test*.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
}
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,15 @@ if __name__ == "__main__":
##what file needs to be located and what variables would need to be changed if you wanted to scrape another website?

-If you wanted to scrape another website, you need to locate the file main.py and change the variables “scraper” and “pages” to whatever website you wanted and the new URl paths. As well ensure the website allows scraping.



Documentation on connecting the database to vscode with the postgres extension

1. Install the PostgreSQL Extension in VSCode
2. Make sure PostgreSQL is Running Locally
3. click the extension on the left sidebar
4. click the plus button and create a new connection
5. fill in the needed information, server = localhost, database = cheaper_local, User = postgres, port = 5432 (default), password = the password you made when installing PostgreSQL
7. You should be connected now and see a message and see the conencted database in the extension now.

Binary file removed accounts/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file removed accounts/__pycache__/admin.cpython-312.pyc
Binary file not shown.
Binary file removed accounts/__pycache__/apps.cpython-312.pyc
Binary file not shown.
Binary file removed accounts/__pycache__/models.cpython-312.pyc
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 5.2 on 2025-05-05 19:14

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("accounts", "0001_initial"),
]

operations = [
migrations.RemoveField(
model_name="product",
name="name",
),
migrations.RemoveField(
model_name="product",
name="source_url",
),
migrations.RemoveField(
model_name="useraccount",
name="password",
),
migrations.AddField(
model_name="product",
name="product_name",
field=models.CharField(default="Unnamed Product", max_length=255),
),
migrations.AddField(
model_name="product",
name="url",
field=models.TextField(default="https://example.com"),
),
migrations.AddField(
model_name="product",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="accounts.useraccount",
),
),
migrations.AddField(
model_name="useraccount",
name="password_hash",
field=models.CharField(default="defaultpass123", max_length=100),
),
migrations.AlterField(
model_name="product",
name="price",
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10),
),
]
Binary file not shown.
Binary file not shown.
13 changes: 8 additions & 5 deletions accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,21 @@ def validate_email(value):

class UserAccount(models.Model):
email = models.EmailField(max_length=50, unique=True)
password = models.CharField(max_length=100)
password_hash = models.CharField(max_length=100)
# password_hash = models.CharField(max_length=100, default='defaultpass123') # added default

def clean(self):
validate_email(self.email)

def __str__(self):
return self.email


class Product(models.Model):
name = models.CharField(max_length=200)
price = models.CharField(max_length=10)
source_url = models.URLField(max_length=150)
product_name = models.CharField(max_length=255, default='Unnamed Product')
price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
url = models.TextField(default='https://example.com')
user = models.ForeignKey(UserAccount, on_delete=models.CASCADE, null=True)

def __str__(self):
return self.name
return self.product_name
7 changes: 7 additions & 0 deletions accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
from django.shortcuts import render
from django.http import JsonResponse
from .models import Product

def product_list(request):
products = Product.objects.all()
data = [{"name": p.product_name, "price": float(p.price), "url": p.url} for p in products]
return JsonResponse(data, safe=False)

# Create your views here.
13 changes: 13 additions & 0 deletions cheaper.egg-info/PKG-INFO
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Metadata-Version: 2.1
Name: cheaper
Version: 0.1
Summary: cheaper for now
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Requires-Dist: beautifulsoup4
Requires-Dist: lxml
Requires-Dist: flask
Requires-Dist: pandas
Requires-Dist: numpy
Requires-Dist: requests
22 changes: 22 additions & 0 deletions cheaper.egg-info/SOURCES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
README.md
setup.py
accounts/__init__.py
accounts/admin.py
accounts/apps.py
accounts/models.py
accounts/tests.py
accounts/views.py
accounts/migrations/0001_initial.py
accounts/migrations/0002_remove_product_name_remove_product_source_url_and_more.py
accounts/migrations/__init__.py
cheaper/__init__.py
cheaper/asgi.py
cheaper/settings.py
cheaper/urls.py
cheaper/wsgi.py
cheaper.egg-info/PKG-INFO
cheaper.egg-info/SOURCES.txt
cheaper.egg-info/dependency_links.txt
cheaper.egg-info/entry_points.txt
cheaper.egg-info/requires.txt
cheaper.egg-info/top_level.txt
1 change: 1 addition & 0 deletions cheaper.egg-info/dependency_links.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

2 changes: 2 additions & 0 deletions cheaper.egg-info/entry_points.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[console_scripts]
cheaper = webscraper.main:main
6 changes: 6 additions & 0 deletions cheaper.egg-info/requires.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
beautifulsoup4
lxml
flask
pandas
numpy
requests
2 changes: 2 additions & 0 deletions cheaper.egg-info/top_level.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
accounts
cheaper
Binary file removed cheaper/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file removed cheaper/__pycache__/settings.cpython-312.pyc
Binary file not shown.
Binary file removed cheaper/__pycache__/urls.cpython-312.pyc
Binary file not shown.
2 changes: 2 additions & 0 deletions cheaper/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"""
from django.contrib import admin
from django.urls import path
from accounts.views import product_list

urlpatterns = [
path('admin/', admin.site.urls),
path('', product_list, name='product_list'), # This sets the homepage
]
Binary file modified db.sqlite3
Binary file not shown.
28 changes: 28 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from setuptools import setup, find_packages

setup(
name='cheaper',
version='0.1',
packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]),
include_package_data=True,
install_requires=[
"beautifulsoup4",
"lxml",
"flask",
"pandas",
"numpy",
"requests",
],
entry_points={
'console_scripts': [
'cheaper=webscraper.main:main',
],
},

description='cheaper for now',
classifiers=[
'Programming Language :: Python :: 3',
'Operating System :: OS Independent',
],
python_requires='>=3.10',
)
Binary file modified webscraper/.DS_Store
Binary file not shown.
15 changes: 15 additions & 0 deletions webscraper/ABC/Ebay_API.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from abc import ABC,abstractmethod

class EbayApi(ABC):

@abstractmethod
def retrieve_access_token() -> str:
""" retrieves the user access token for sandbox environment it's a long line
of text, numbers, symbols
"""
pass

@abstractmethod
def retrieve_ebay_response(httprequest:str,query:str) -> dict:
""" retrieves a json of large data with category ids, names, parentcategorynodes """
pass
Binary file not shown.
55 changes: 55 additions & 0 deletions webscraper/api/EbayAPI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import requests
from requests.auth import HTTPBasicAuth
from dotenv import load_dotenv
import os


load_dotenv() #initialize

class EbayAPI:

client_secret_key = os.getenv("clientsecret")
client_id_key = os.getenv("clientid")

get_user_key = HTTPBasicAuth(client_id_key, client_secret_key)


def retrieve_access_token():
try:
response = requests.post("https://api.sandbox.ebay.com/identity/v1/oauth2/token",
headers = {"Content-Type":"application/x-www-form-urlencoded"},
data = {
"grant_type": "client_credentials",
"scope": "https://api.ebay.com/oauth/api_scope"
},
auth=EbayAPI.get_user_key
)
access_token = response.json().get("access_token")
status_code = response.status_code
if(status_code == 404):
raise Exception("404 error here")
return access_token
except Exception as e:
raise e

def retrieve_ebay_response(httprequest:str,query:str):
auth = EbayAPI.retrieve_access_token()
try:
response = requests.get(httprequest,
headers={
"Authorization": f"Bearer {auth}",
"Content-Type": "application/json"
},
params= {
"q": query,
"category_tree_id": 0
}
)
status_code = response.status_code
if(status_code == 404):
raise Exception("not found 404 error")

return response.json()
except Exception as e:
raise e

Binary file removed webscraper/api/__pycache__/interface.cpython-311.pyc
Binary file not shown.
Binary file removed webscraper/api/__pycache__/routes.cpython-311.pyc
Binary file not shown.
Binary file not shown.
35 changes: 35 additions & 0 deletions webscraper/api/tests/test_ebay_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import unittest
from unittest.mock import patch,Mock
import requests
from webscraper.api.EbayAPI import EbayAPI

class EbayTestApi(unittest.TestCase):

def setUp(self):
self.EbayAPI = EbayAPI


def test_retrieve_access_token(self):
self.EbayAPI.retrieve_access_token()
self.assertEqual(type(self.EbayAPI.retrieve_access_token()),str)

@patch("webscraper.api.EbayAPI.requests.post")
def test_retrieve_access_token_invalid(self,mock_post):
mock_response = Mock()
mock_response.status_code = 404
mock_response.json.return_value ={"error": "not found"}
mock_post.return_value = mock_response

with self.assertRaises(Exception):
self.EbayAPI.retrieve_access_token()



@patch("webscraper.api.EbayAPI.requests.get")
def test_retrieve_ebay_response_invalid(self,mock_get):
self.EbayAPI.retrieve_ebay_response("https://test","item")
self.assertRaises(Exception)


if __name__ == '__main__':
unittest.main()
Loading