diff --git a/app/__init__.py b/app/__init__.py index 33116d4..8ddff71 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,4 @@ """Python - FastAPI, Postgres, tsvector""" # Current Version -__version__ = "2.2.4" +__version__ = "2.2.5" diff --git a/app/api/queue/__init__.py b/app/api/queue/__init__.py index d4da595..420be2c 100644 --- a/app/api/queue/__init__.py +++ b/app/api/queue/__init__.py @@ -1,3 +1,20 @@ """Queue Routes""" -from .queue import router as queue_router \ No newline at end of file + +from fastapi import APIRouter +from .routes.drop import router as drop_router +from .routes.empty import router as empty_router +from .routes.get import router as get_router + +from .routes.create import router as create_router + +from .routes.import_linkedin import router as import_linkedin_router +from .routes.alter import router as alter_router + +router = APIRouter() +router.include_router(drop_router) +router.include_router(empty_router) +router.include_router(get_router) +router.include_router(create_router) +router.include_router(import_linkedin_router) +router.include_router(alter_router) \ No newline at end of file diff --git a/app/static/csv/apollo-contacts-export (3).csv b/app/api/queue/csv/apollo/apollo-contacts-export (3).csv similarity index 100% rename from app/static/csv/apollo-contacts-export (3).csv rename to app/api/queue/csv/apollo/apollo-contacts-export (3).csv diff --git a/app/static/csv/apollo-contacts-export (4).csv b/app/api/queue/csv/apollo/apollo-contacts-export (4).csv similarity index 100% rename from app/static/csv/apollo-contacts-export (4).csv rename to app/api/queue/csv/apollo/apollo-contacts-export (4).csv diff --git a/app/static/csv/big.csv b/app/api/queue/csv/apollo/big.csv similarity index 100% rename from app/static/csv/big.csv rename to app/api/queue/csv/apollo/big.csv diff --git a/app/static/csv/seed.csv b/app/api/queue/csv/apollo/seed.csv similarity index 100% rename from app/static/csv/seed.csv rename to app/api/queue/csv/apollo/seed.csv diff --git a/app/api/prospects/linkedin.csv b/app/api/queue/csv/linkedin/linkedin.csv similarity index 100% rename from app/api/prospects/linkedin.csv rename to app/api/queue/csv/linkedin/linkedin.csv diff --git a/app/api/queue/csv/linkedin/linkedin_sample.csv b/app/api/queue/csv/linkedin/linkedin_sample.csv new file mode 100644 index 0000000..cf01c55 --- /dev/null +++ b/app/api/queue/csv/linkedin/linkedin_sample.csv @@ -0,0 +1,11 @@ +First Name,Last Name,URL,Email Address,Company,Position,Connected On +Manny,Okene,https://www.linkedin.com/in/manny-o-462924138,,Premier Group Recruitment,Associate Director,29 Mar 2026 +Mark Keers -,Pulse IT Recruitment Ltd,https://www.linkedin.com/in/markkeers,,Pulse IT Recruitment Ltd,IT Recruiter & Managing Director - 07500 734616,26 Mar 2026 +Shaun,O'Donnell,https://www.linkedin.com/in/shaunodonnell,,Erin Associates Ltd,Director,12 Mar 2026 +Kai,Kemp,https://www.linkedin.com/in/kaikemp97,,Technify Talent,Principal Consultant,04 Mar 2026 +James,Ward,https://www.linkedin.com/in/james-ward-echopay,,EchoPay Technology Ltd,Managing Director,04 Mar 2026 +Graham,Feegan,https://www.linkedin.com/in/grahamfeegan,,Charles Jenson Recruitment,Recruitment Director,18 Feb 2026 +Nabil,Cook,https://www.linkedin.com/in/nabilcook,,OMEGA Solutions,Co-Founder & Technical Director,11 Feb 2026 +Oliver,Ward,https://www.linkedin.com/in/oliver-ward-6057a1262,,EchoPay,Customer Success Manager,11 Feb 2026 +🟢 Brad,Leaman,https://www.linkedin.com/in/bradleaman,,F5 Consultants,Principal Recruitment Consultant & Co-Founder,08 Feb 2026 +Rupal,Gupta,https://www.linkedin.com/in/rupal-gupta-a66975214,,PRACYVA ,Senior IT Recruiter (UK/EU),04 Feb 2026 diff --git a/app/api/orders/sql/magento_products.csv b/app/api/queue/csv/magento/magento_products.csv similarity index 100% rename from app/api/orders/sql/magento_products.csv rename to app/api/queue/csv/magento/magento_products.csv diff --git a/app/static/csv/magento_products_sample.csv b/app/api/queue/csv/magento/magento_products_sample.csv similarity index 100% rename from app/static/csv/magento_products_sample.csv rename to app/api/queue/csv/magento/magento_products_sample.csv diff --git a/app/api/queue/queue.py b/app/api/queue/queue.py index cae6430..5984aed 100644 --- a/app/api/queue/queue.py +++ b/app/api/queue/queue.py @@ -1,15 +1,8 @@ -import os -from fastapi import APIRouter, HTTPException, Query, Request, Depends -from app.utils.make_meta import make_meta -from app.utils.db import get_db_connection_direct -from app.utils.api_key_auth import get_api_key -router = APIRouter() - -@router.get("/queue") -def read_queue() -> dict: - """GET /queue: """ - return {"meta": make_meta("success", "Hello from queue"), "data": {"do": "it"}} +from .routes.drop import router as drop_router +from .routes.empty import router as empty_router +from .routes.get import router as get_router +from .routes.create import router as create_router diff --git a/app/api/queue/routes/alter.py b/app/api/queue/routes/alter.py new file mode 100644 index 0000000..954ff5c --- /dev/null +++ b/app/api/queue/routes/alter.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, HTTPException, Body +from app.utils.make_meta import make_meta +from app.utils.db import get_db_connection_direct + +router = APIRouter() + +@router.post("/queue/alter/add-column") +def add_column_to_queue( + column_name: str = Body(..., embed=True), + column_type: str = Body(..., embed=True) +) -> dict: + """POST /queue/alter/add-column: Add a new column to the queue table.""" + try: + conn = get_db_connection_direct() + cursor = conn.cursor() + sql = f'ALTER TABLE queue ADD COLUMN "{column_name}" {column_type};' + cursor.execute(sql) + conn.commit() + conn.close() + return {"meta": make_meta("success", f"Column '{column_name}' added as {column_type}")} + except Exception as e: + msg = str(e) + if 'already exists' in msg: + return {"meta": make_meta("error", f"Column '{column_name}' exists")} + raise HTTPException(status_code=500, detail=msg) diff --git a/app/api/queue/routes/create.py b/app/api/queue/routes/create.py new file mode 100644 index 0000000..a0cbff2 --- /dev/null +++ b/app/api/queue/routes/create.py @@ -0,0 +1,25 @@ +import os +from fastapi import APIRouter, HTTPException +from app.utils.make_meta import make_meta +from app.utils.db import get_db_connection_direct + +router = APIRouter() + +@router.post("/queue/create") +def create_queue_table() -> dict: + """POST /queue/create: Create the queue table from SQL script.""" + try: + sql_path = os.path.join(os.path.dirname(__file__), "../sql/create_queue_table.sql") + with open(sql_path, "r") as f: + sql = f.read() + conn = get_db_connection_direct() + cursor = conn.cursor() + # Split SQL script into individual statements for PostgreSQL + statements = [s.strip() for s in sql.split(';') if s.strip()] + for statement in statements: + cursor.execute(statement) + conn.commit() + conn.close() + return {"meta": make_meta("success", "Queue table created")} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/api/queue/routes/drop.py b/app/api/queue/routes/drop.py new file mode 100644 index 0000000..bb75946 --- /dev/null +++ b/app/api/queue/routes/drop.py @@ -0,0 +1,19 @@ +import os +from fastapi import APIRouter, HTTPException +from app.utils.make_meta import make_meta +from app.utils.db import get_db_connection_direct + +router = APIRouter() + +@router.post("/queue/drop") +def drop_queue_table() -> dict: + """POST /queue/drop: Drop the queue table.""" + try: + conn = get_db_connection_direct() + cursor = conn.cursor() + cursor.execute("DROP TABLE IF EXISTS queue;") + conn.commit() + conn.close() + return {"meta": make_meta("success", "Queue table dropped")} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/api/queue/routes/empty.py b/app/api/queue/routes/empty.py new file mode 100644 index 0000000..2bb1df5 --- /dev/null +++ b/app/api/queue/routes/empty.py @@ -0,0 +1,19 @@ +import os +from fastapi import APIRouter, HTTPException +from app.utils.make_meta import make_meta +from app.utils.db import get_db_connection_direct + +router = APIRouter() + +@router.post("/queue/empty") +def empty_queue_table() -> dict: + """POST /queue/empty: Remove all records from the queue table.""" + try: + conn = get_db_connection_direct() + cursor = conn.cursor() + cursor.execute("DELETE FROM queue;") + conn.commit() + conn.close() + return {"meta": make_meta("success", "Queue table emptied")} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/api/queue/routes/get.py b/app/api/queue/routes/get.py new file mode 100644 index 0000000..d838a0e --- /dev/null +++ b/app/api/queue/routes/get.py @@ -0,0 +1,47 @@ +import os +from fastapi import APIRouter, HTTPException +from app.utils.make_meta import make_meta +from app.utils.db import get_db_connection_direct + +router = APIRouter() + +@router.get("/queue") +def read_queue() -> dict: + """GET /queue: Return queue table info, schema, and most recent record.""" + try: + conn = get_db_connection_direct() + cursor = conn.cursor() + + # 1. Count records + cursor.execute("SELECT COUNT(*) FROM queue;") + count_row = cursor.fetchone() + record_count = count_row[0] if count_row else 0 + + # 2. Get table schema + cursor.execute("SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = 'queue';") + schema = [ + { + "name": row[0], + "type": row[1] + } + for row in cursor.fetchall() + ] + + # 3. Get most recently updated record + cursor.execute("SELECT * FROM queue ORDER BY updated DESC LIMIT 1;") + columns = [desc[0] for desc in cursor.description] if cursor.description else [] + row = cursor.fetchone() + most_recent = dict(zip(columns, row)) if row and columns else None + + conn.close() + + return { + "meta": make_meta("success", "Queue table info"), + "data": { + "queued": record_count, + "most_recent": most_recent, + # "schema": schema + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/api/queue/routes/import_linkedin.py b/app/api/queue/routes/import_linkedin.py new file mode 100644 index 0000000..a8c97b9 --- /dev/null +++ b/app/api/queue/routes/import_linkedin.py @@ -0,0 +1,46 @@ +import os +import csv +import time +from fastapi import APIRouter, HTTPException +from app.utils.make_meta import make_meta +from app.utils.db import get_db_connection_direct + +router = APIRouter() + +@router.post("/queue/import/linkedin") +def import_linkedin_csv() -> dict: + """POST /queue/import/linkedin: Import data from linkedin_sample.csv into the queue table.""" + csv_path = os.path.join(os.path.dirname(__file__), "../csv/linkedin/linkedin_sample.csv") + if not os.path.exists(csv_path): + raise HTTPException(status_code=404, detail="linkedin_sample.csv not found") + try: + conn = get_db_connection_direct() + cursor = conn.cursor() + with open(csv_path, newline='', encoding='utf-8') as csvfile: + reader = csv.DictReader(row for row in csvfile if not row.startswith('Notes:')) + now = int(time.time()) + for row in reader: + cursor.execute( + """ + INSERT INTO queue (first_name, last_name, url, email_address, company, position, connected_on, created, updated, hidden, collection) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + [ + row.get('First Name'), + row.get('Last Name'), + row.get('URL'), + row.get('Email Address'), + row.get('Company'), + row.get('Position'), + row.get('Connected On'), + now, + now, + False, + 'prospects' + ] + ) + conn.commit() + conn.close() + return {"meta": make_meta("success", "LinkedIn CSV imported")} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/api/routes.py b/app/api/routes.py index d1446b8..b10e210 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -13,7 +13,7 @@ from app.api.prompt.drop import router as drop_router from app.api.prospects.prospects import router as prospects_router from app.api.orders.orders import router as orders_router -from app.api.queue.queue import router as queue_router +from app.api.queue import router as queue_router router.include_router(root_router) router.include_router(resend_router) diff --git a/app/static/csv/magento_products.csv b/app/static/csv/magento_products.csv deleted file mode 100644 index 2dbd9a7..0000000 --- a/app/static/csv/magento_products.csv +++ /dev/null @@ -1,4247 +0,0 @@ -sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,associated_skus,downloadable_links,downloadable_samples,configurable_variations,configurable_variation_labels -24-MB01,,Bag,simple,"Default Category/Gear,Default Category/Gear/Bags",base,"Joust Duffle Bag","

The sporty Joust Duffle Bag can't be beat - not in the gym, not on the luggage carousel, not anywhere. Big enough to haul a basketball or soccer ball and some sneakers with plenty of room to spare, it's ideal for athletes with places to go.

-

",,,1,,"Catalog, Search",34.000000,,,,joust-duffle-bag,,,,/m/b/mb01-blue-0.jpg,,/m/b/mb01-blue-0.jpg,,/m/b/mb01-blue-0.jpg,,,,2/5/26,2/5/26,,,,,,,,,,,,,,,,,100.0000,0.0000,1,0,0,1,1,1,10000,1,1,,1,1,1,1,0.0000,1,0,0,0,,,"24-WG086,24-WG083-blue,24-UG01,24-WG085_Group","1,2,3,4","24-MB02,24-MB03,24-MB05,24-MB06,24-UB02,24-WB03,24-WB04,24-WB07","1,2,3,4,5,6,7,8",/m/b/mb01-blue-0.jpg,Image,,,,,,,,,,,,, -24-MB04,,Bag,simple,"Default Category/Gear,Default Category/Collections,Default Category/Gear/Bags",base,"Strive Shoulder Pack","

Convenience is next to nothing when your day is crammed with action. So whether you're heading to class, gym, or the unbeaten path, make sure you've got your Strive Shoulder Pack stuffed with all your essentials, and extras as well.

-",,,1,"Taxable Goods","Catalog, Search",32.000000,32.000000,2/5/26,,strive-shoulder-pack,,,,/m/b/mb04-black-0.jpg,,/m/b/mb04-black-0.jpg,,/m/b/mb04-black-0.jpg,,,,2/5/26,2/5/26,,,"Block after Info Column",,,,,,,,,,,"Use config",,"activity=Gym|Hiking|Trail|Urban,erin_recommends=Yes,features_bags=Audio Pocket|Waterproof|Lightweight|Laptop Sleeve,material=Canvas|Cotton|Mesh|Polyester,sale=Yes,strap_bags=Adjustable|Cross Body|Padded|Shoulder|Single,style_bags=Messenger|Exercise|Tote",100.0000,0.0000,1,0,0,1,1,1,10000,1,1,,1,1,1,1,0.0000,1,0,0,0,,,"24-UG03,24-UG05,24-WG080,24-UG04","1,2,3,4","24-MB01,24-MB02,24-MB03,24-MB05,24-MB06,24-UB02,24-WB03,24-WB04,24-WB06,24-WB07","1,2,3,4,5,6,7,8,9,10","/m/b/mb04-black-0.jpg,/m/b/mb04-black-0_alt1.jpg","Image,Image",,,,,,,,,,,,, -24-MB03,,Bag,simple,"Default Category/Gear,Default Category/Gear/Bags",base,"Crown Summit Backpack","

The Crown Summit Backpack is equally at home in a gym locker, study cube or a pup tent, so be sure yours is packed with books, a bag lunch, water bottles, yoga block, laptop, or whatever else you want in hand. Rugged enough for day hikes and camping trips, it has two large zippered compartments and padded, adjustable shoulder straps.

-