1212- Sample data for events and news is kept in-memory for simplicity.
1313"""
1414
15+ import os
16+ import json
1517from datetime import date
1618from typing import List , Optional
19+ from supabase import create_client , Client
20+ from dotenv import load_dotenv
1721
1822from fastapi import FastAPI , HTTPException , Request
1923from fastapi .responses import HTMLResponse , JSONResponse , RedirectResponse
2024from fastapi .staticfiles import StaticFiles
2125from fastapi .templating import Jinja2Templates
2226from pydantic import BaseModel
27+ from email_validator import validate_email , EmailNotValidError
2328
2429app = FastAPI (title = "Python Togo" )
2530
2631# Mount static files
2732app .mount ("/static" , StaticFiles (directory = "static" ), name = "static" )
2833
2934templates = Jinja2Templates (directory = "templates" )
35+ load_dotenv ()
36+ SUBABASE_URL = os .getenv ("SUPABASE_URL" )
37+ SUPABASE_KEY = os .getenv ("SUPABASE_KEY" )
3038
39+ supabase : Client = create_client (SUBABASE_URL , SUPABASE_KEY )
3140
3241# Simple in-memory sample data
3342SAMPLE_EVENTS = [
8493 {
8594 "id" : 1 ,
8695 "date" : "2025-11-01" ,
87- # Optional image URL for this news item; if missing, a placeholder will be used
8896 "image" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1763440928/20251117_200618_tsfc73.jpg" ,
8997 "translations" : {
9098 "fr" : {
156164 },
157165]
158166
159- SAMPLE_COMMUNITIES = [
160- {
161- "id" : 1 ,
162- "name" : "Python Lomé" ,
163- "description" : "Groupe local basé à Lomé." ,
164- "city" : "Lomé" ,
165- }
166- ]
167167
168168TRANSLATIONS = {
169169 "fr" : {
@@ -873,12 +873,6 @@ class JoinRequest(BaseModel):
873873 interests : str | None = None
874874
875875
876- class ContactRequest (BaseModel ):
877- name : str
878- email : str
879- subject : str
880- message : str
881-
882876
883877class PartnershipRequest (BaseModel ):
884878 organization : str
@@ -888,15 +882,6 @@ class PartnershipRequest(BaseModel):
888882 message : Optional [str ] = None
889883
890884
891- class JoinSubmit (BaseModel ):
892- name : str
893- email : str
894- city : Optional [str ] = None
895- level : Optional [str ] = None
896- agree_privacy : bool
897- agree_coc : bool
898-
899-
900885class ContactSubmit (BaseModel ):
901886 name : str
902887 email : str
@@ -906,94 +891,50 @@ class ContactSubmit(BaseModel):
906891 agree_coc : bool
907892
908893
909- # In-memory holders for partnership requests and partners
910- PARTNERS = [
911- {
912- "name" : "Python Software Foundation" ,
913- "website" : "https://www.python.org/" ,
914- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/b_rgb:457FAB/c_crop,w_1000,h_563,ar_16:9,g_auto/v1750463364/psf-logo_gqppfi.png" ,
915- },
916- {
917- "name" : "Association Francophone de Python" ,
918- "website" : "https://www.afpy.org/" ,
919- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1757104352/afpy_mizqfd.png" ,
920- },
921- {
922- "name" : "Django Software Foundation" ,
923- "website" : "https://www.djangoproject.com/" ,
924- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1757104362/django-logo-positive_ziry9u.png" ,
925- },
926- {
927- "name" : "Black Python Devs" ,
928- "website" : "https://blackpythondevs.com/index.html" ,
929- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1751558100/bpd_stacked_us5ika.png" ,
930- },
931- {
932- "name" : "O'REILLY" ,
933- "website" : "https://www.oreilly.com/" ,
934- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1763335940/oreilly_logo_mark_red_efvlhr.svg" ,
935- },
936- {
937- "name" : "GitBook" ,
938- "website" : "https://www.gitbook.com/" ,
939- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1763336328/GitBook_-_Dark_igckn1.png" ,
940- },
941- {
942- "name" : "Tahaga" ,
943- "website" : "https://tahaga.com/" ,
944- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1753966042/Logo_TAHAGA_02_Plan_de_travail_1_5_rh5s9g.jpg" ,
945- },
946- {
947- "name" : "DijiJob" ,
948- "website" : "https://wearedigijob.com" ,
949- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1755176153/digijoblogo_tkbhns.png" ,
950- },
951- {
952- "name" : "Microsoft Student Ambassadors Togo" ,
953- "website" : "https://www.youtube.com/@mlsatogo" ,
954- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1754294585/msftatogo_qrtxl9.png" ,
955- },
956- {
957- "name" : "GitHub" ,
958- "website" : "https://github.com/" ,
959- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1752712570/GitHub_Logo_pn7gcn.png" ,
960- },
961- {
962- "name" : "Kweku Tech" ,
963- "website" : "https://www.youtube.com/@KwekuTech" ,
964- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1754092403/kwekutech_v4l7qn.png" ,
965- },
966- {
967- "name" : "AllDotPy" ,
968- "website" : "https://alldotpy.org/" ,
969- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1755768009/dotpy_blue_transparent_irvs5g.png" ,
970- },
971- {
972- "name" : "IJEAF" ,
973- "website" : "https://www.linkedin.com/company/ijeaf/" ,
974- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1755768049/ijeaf_qjqsmf.jpg" ,
975- },
976- {
977- "name" : "TeDxVItChannai" ,
978- "website" : "https://www.ted.com/tedx/events/19414" ,
979- "logo_url" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1754246473/tedxVitchennai_d5bkda.png" ,
980- },
981- ]
982- GALLERIES = [
983- {
984- "date" : "2025-08-23" ,
985- "title" : "PyCon Togo 2025" ,
986- "image" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1747588996/Group_6_r7n6id.png" ,
987- "link" : "https://drive.google.com/drive/folders/1Xk8lejAQXBIPjPf1UHnuUmZJ5sEYNPM1?usp=sharing" ,
988- },
989- {
990- "date" : "2024-11-30" ,
991- "title" : "PyDay Togo 2024" ,
992- "image" : "https://res.cloudinary.com/dvg7vky5o/image/upload/v1763346354/WUL_0330_rnemnd.jpg" ,
993- "link" : "https://drive.google.com/drive/folders/1UCZgBjcAaztwCi5R74WRqI8oxbCe1DNC?usp=sharing" ,
994- },
995- ]
996- PARTNERSHIP_REQUESTS : List [dict ] = []
894+ def get_data (table ):
895+ """Fetch all data from a given Supabase table.
896+
897+ Parameters
898+ ----------
899+ table : str
900+ The name of the table to query.
901+
902+ Returns
903+ -------
904+ list of dict
905+ The list of records from the table, or empty list on error.
906+ """
907+ try :
908+ response = supabase .table (table ).select ("*" ).execute ()
909+ return response .data
910+ except Exception :
911+ return []
912+
913+ def insert_data (table , data ):
914+ """Insert data into a given Supabase table.
915+
916+ Parameters
917+ ----------
918+ table : str
919+ The name of the table to insert into.
920+ data : dict
921+ The data to insert.
922+ Returns
923+ -------
924+ bool
925+ True if insertion was successful, False otherwise.
926+ """
927+ try :
928+ supabase .table (table ).insert (data ).execute ()
929+ return True
930+ except Exception as e :
931+ print (f"Error inserting data into { table } : { e } " )
932+ return False
933+
934+ PARTNERS = get_data ("partners" )
935+
936+ GALLERIES = get_data ("galleries" )
937+
997938JOIN_REQUESTS : List [dict ] = []
998939CONTACT_MESSAGES : List [dict ] = []
999940
@@ -1347,9 +1288,33 @@ async def partnership_submit(request: PartnershipRequest):
13471288 --------
13481289 ``POST /api/v1/partnership`` with JSON body
13491290 """
1350- PARTNERSHIP_REQUESTS .append (request .dict ())
1351- # Here you would normally send an email or store the request in a DB
1352- return JSONResponse (content = {"status" : "received" })
1291+ data = {}
1292+ ct = request .headers .get ("content-type" , "" )
1293+ if "application/json" in ct :
1294+ data = await request .json ()
1295+ data = PartnershipRequest (** data )
1296+ else :
1297+ form = await request .form ()
1298+ data = dict (form )
1299+ data = PartnershipRequest (** data )
1300+
1301+ try :
1302+ validate_email (data .email , check_deliverability = True )
1303+ except EmailNotValidError as e :
1304+ return JSONResponse (status_code = 400 , content = {"error" : "Please use a valide email" })
1305+
1306+ # Normalize boolean fields
1307+ agree_privacy = data .agree_privacy in (True , "true" , "True" , "on" , "1" , 1 )
1308+ agree_coc = data .agree_coc in (True , "true" , "True" , "on" , "1" , 1 )
1309+ if not agree_privacy or not agree_coc :
1310+ return JSONResponse (status_code = 400 , content = {"error" : "consent_required" })
1311+
1312+ data = json .dumps (data .dict ())
1313+ inserted = insert_data ("partnershiprequest" , data )
1314+ if inserted :
1315+ return JSONResponse (content = {"status" : "received" })
1316+ else :
1317+ return JSONResponse (content = {"status" : "Failed" })
13531318
13541319
13551320@app .post ("/api/v1/join" )
@@ -1374,19 +1339,29 @@ async def join_submit(request: Request):
13741339 ct = request .headers .get ("content-type" , "" )
13751340 if "application/json" in ct :
13761341 data = await request .json ()
1342+ data = JoinRequest (** data )
13771343 else :
13781344 form = await request .form ()
13791345 data = dict (form )
1346+ data = JoinRequest (** data )
1347+
1348+ try :
1349+ validate_email (data .email , check_deliverability = True )
1350+ except EmailNotValidError as e :
1351+ return JSONResponse (status_code = 400 , content = {"error" : "Please use a valide email" })
13801352
13811353 # Normalize boolean fields
1382- agree_privacy = data .get ( " agree_privacy" ) in (True , "true" , "True" , "on" , "1" , 1 )
1383- agree_coc = data .get ( " agree_coc" ) in (True , "true" , "True" , "on" , "1" , 1 )
1354+ agree_privacy = data .agree_privacy in (True , "true" , "True" , "on" , "1" , 1 )
1355+ agree_coc = data .agree_coc in (True , "true" , "True" , "on" , "1" , 1 )
13841356 if not agree_privacy or not agree_coc :
13851357 return JSONResponse (status_code = 400 , content = {"error" : "consent_required" })
1386-
1387- # store
1388- JOIN_REQUESTS .append (data )
1389- return JSONResponse (content = {"status" : "received" })
1358+
1359+ data = json .dumps (data .dict ())
1360+ inserted = insert_data ("members" , data )
1361+ if inserted :
1362+ return JSONResponse (content = {"status" : "received" })
1363+ else :
1364+ return JSONResponse (content = {"status" : "Failed" })
13901365
13911366
13921367@app .post ("/api/v1/contact" )
@@ -1408,19 +1383,33 @@ async def contact_submit(request: Request):
14081383 """
14091384 data = {}
14101385 ct = request .headers .get ("content-type" , "" )
1386+
14111387 if "application/json" in ct :
14121388 data = await request .json ()
1389+ data = ContactSubmit (** data ) # validate
14131390 else :
14141391 form = await request .form ()
14151392 data = dict (form )
1416-
1417- agree_privacy = data .get ("agree_privacy" ) in (True , "true" , "True" , "on" , "1" , 1 )
1418- agree_coc = data .get ("agree_coc" ) in (True , "true" , "True" , "on" , "1" , 1 )
1393+ data = ContactSubmit (** data )
1394+
1395+ try :
1396+ validate_email (data .email , check_deliverability = True )
1397+ except EmailNotValidError as e :
1398+ return JSONResponse (status_code = 400 , content = {"error" : "Please use a valide email" })
1399+
1400+ agree_privacy = data .agree_coc in (True , "true" , "True" , "on" , "1" , 1 )
1401+ agree_coc = data .agree_coc in (True , "true" , "True" , "on" , "1" , 1 )
14191402 if not agree_privacy or not agree_coc :
14201403 return JSONResponse (status_code = 400 , content = {"error" : "consent_required" })
14211404
1422- CONTACT_MESSAGES .append (data )
1423- return JSONResponse (content = {"status" : "received" })
1405+ data = json .dumps (data .dict ())
1406+
1407+ inserted = insert_data ("contacts" , data )
1408+ if inserted :
1409+ return JSONResponse (content = {"status" : "received" })
1410+ else :
1411+ return JSONResponse (content = {"status" : "Failed" })
1412+
14241413
14251414
14261415@app .get ("/gallery" )
0 commit comments