22# dependencies = [
33# "httpx",
44# "python-dotenv",
5+ # "python-ulid>=3.0.0",
6+ # "typing-extensions",
7+ # "colorlog",
58# ]
69# ///
710
811# NOTE: Please read ./README.md
912
13+ import json
14+ import typing
1015import httpx
1116import logging
17+ import colorlog
1218from dotenv import dotenv_values
19+ from ulid import ULID
1320
14- logger = logging .getLogger (__name__ )
1521config = dotenv_values (".env" )
1622
1723
24+ def logging_init ():
25+ handler = colorlog .StreamHandler ()
26+ handler .setFormatter (
27+ colorlog .ColoredFormatter (
28+ "%(log_color)s[%(levelname)s]%(reset)s %(message)s" ,
29+ log_colors = {
30+ "DEBUG" : "cyan" ,
31+ "INFO" : "green" ,
32+ "WARNING" : "yellow" ,
33+ "ERROR" : "red" ,
34+ "CRITICAL" : "bold_red" ,
35+ },
36+ )
37+ )
38+
39+ logger = colorlog .getLogger ()
40+ logger .addHandler (handler )
41+ logger .setLevel (logging .INFO )
42+ return logger
43+
44+
45+ logger = logging_init ()
46+
47+
1848# Define the GraphQL query
1949class Query :
20-
50+ ME_OP_NAME = "Me"
2151 ME = """
22- query MyQuery {
52+ query Me {
2353 me {
2454 id
2555 displayName
2656 }
2757 }
2858 """
2959
60+ PUBLIC_PROJECTS_OP_NAME = "PublicProjectsList"
3061 PUBLIC_PROJECTS = """
31- query MyQuery ($filters: ProjectFilter = {}) {
62+ query PublicProjectsList ($filters: ProjectFilter = {}) {
3263 publicProjects(filters: $filters) {
3364 totalCount
3465 results {
@@ -102,8 +133,9 @@ class Query:
102133 }
103134 """
104135
136+ PROJECTS_OP_NAME = "ProjectsList"
105137 PROJECTS = """
106- query MyQuery {
138+ query ProjectsList {
107139 projects {
108140 totalCount
109141 results {
@@ -177,13 +209,73 @@ class Query:
177209 }
178210 """
179211
180- class MapswipeApi :
212+ ORGANIZATIONS_OP_NAME = "Organizations"
213+ ORGANIZATIONS = """
214+ query Organizations {
215+ organizations {
216+ totalCount
217+ results {
218+ id
219+ name
220+ }
221+ }
222+ }
223+ """
224+
225+ CREATE_DRAFT_PROJECTS_OP_NAME = "NewDraftProject"
226+ CREATE_DRAFT_PROJECTS = """
227+ mutation NewDraftProject($data: ProjectCreateInput!) {
228+ createProject(data: $data) {
229+ ... on OperationInfo {
230+ __typename
231+ messages {
232+ code
233+ field
234+ kind
235+ message
236+ }
237+ }
238+ ... on ProjectTypeMutationResponseType {
239+ errors
240+ ok
241+ result {
242+ id
243+ firebaseId
244+ }
245+ }
246+ }
247+ }
248+ """
249+
250+ CREATE_PROJECT_ASSET_OP_NAME = "CreateProjectAsset"
251+ CREATE_PROJECT_ASSET = """
252+ mutation CreateProjectAsset($data: ProjectAssetCreateInput!) {
253+ createProjectAsset(data: $data) {
254+ ... on ProjectAssetTypeMutationResponseType {
255+ errors
256+ ok
257+ result {
258+ id
259+ file {
260+ name
261+ url
262+ }
263+ }
264+ }
265+ }
266+ }
267+ """
268+
269+
270+ class MapSwipeApiClient :
181271 # Set the base URL
182272 BASE_URL = config ["BACKEND_URL" ]
183273 CSRFTOKEN_KEY = config ["CSRFTOKEN_KEY" ]
184274 MANAGER_URL = config ["MANAGER_URL" ]
185275
186- ENABLE_AUTHENTICATION = config .get ("ENABLE_AUTHENTICATION" , "false" ).lower () == "true"
276+ ENABLE_AUTHENTICATION = (
277+ config .get ("ENABLE_AUTHENTICATION" , "false" ).lower () == "true"
278+ )
187279 FB_AUTH_URL = config .get ("FB_AUTH_URL" )
188280
189281 # Your web-app login credential
@@ -202,7 +294,6 @@ def __enter__(self):
202294
203295 csrf_token = self .client .cookies .get (self .CSRFTOKEN_KEY )
204296 self .headers = {
205- "content-type" : "application/json" ,
206297 # Required for CSRF verification
207298 "x-csrftoken" : csrf_token ,
208299 "origin" : self .MANAGER_URL ,
@@ -239,26 +330,123 @@ def login_with_firebaes(self):
239330 )
240331 resp .raise_for_status ()
241332
242- def graphql_request (self , query , variables = None ):
333+ def graphql_request_with_files (
334+ self ,
335+ operation_name : str ,
336+ query : str ,
337+ * ,
338+ files : dict [typing .Any , typing .Any ],
339+ map : dict [typing .Any , typing .Any ],
340+ variables : dict [typing .Any , typing .Any ] | None = None ,
341+ ):
342+ # Request type: form data
243343 graphql_resp = self .client .post (
244344 "/graphql/" ,
245345 headers = self .headers ,
246- json = {
247- "query" : query ,
248- "variables" : variables ,
346+ files = files ,
347+ data = {
348+ "operations" : json .dumps (
349+ {
350+ "query" : query ,
351+ "variables" : variables ,
352+ },
353+ ),
354+ "map" : json .dumps (map ),
249355 },
250356 )
251357
358+ if not (200 <= graphql_resp .status_code < 300 ):
359+ logger .error ("Error: %s" , graphql_resp .text )
252360 graphql_resp .raise_for_status ()
253361
254362 return graphql_resp .json ()
255363
364+ def graphql_request (
365+ self ,
366+ operation_name : str ,
367+ query : str ,
368+ variables : dict [typing .Any , typing .Any ] | None = None ,
369+ ):
370+ payload = {
371+ "operationName" : operation_name ,
372+ "query" : query ,
373+ "variables" : variables ,
374+ }
375+
376+ graphql_resp = self .client .post (
377+ "/graphql/" ,
378+ headers = self .headers ,
379+ json = payload ,
380+ )
381+
382+ if not (200 <= graphql_resp .status_code < 300 ):
383+ logger .error ("Error: %s" , graphql_resp .text )
384+ graphql_resp .raise_for_status ()
385+
386+ return graphql_resp .json ()
256387
257- with MapswipeApi () as api :
258- print ('Public endpoints' )
388+ def create_draft_project (self , params ):
389+ resp = self .graphql_request (
390+ Query .CREATE_DRAFT_PROJECTS_OP_NAME ,
391+ Query .CREATE_DRAFT_PROJECTS ,
392+ {"data" : params },
393+ )
394+
395+ if errors := resp .get ("errors" ):
396+ logger .error ("Failed to create new project: %s" , errors )
397+ return None
398+
399+ if errors := resp ["data" ]["createProject" ].get ("messages" ):
400+ logger .error ("Failed to create new project: %s" , errors )
401+ return None
402+
403+ if errors := resp ["data" ]["createProject" ].get ("errors" ):
404+ logger .error ("Failed to create new project: %s" , errors )
405+ return None
406+
407+ return resp ["data" ]["createProject" ]["result" ]["id" ]
408+
409+ def create_project_asset (
410+ self ,
411+ * ,
412+ project_file ,
413+ params ,
414+ ):
415+ resp = self .graphql_request_with_files (
416+ Query .CREATE_PROJECT_ASSET_OP_NAME ,
417+ Query .CREATE_PROJECT_ASSET ,
418+ files = {
419+ "projectFile" : project_file ,
420+ },
421+ map = {
422+ "projectFile" : ["variables.data.file" ],
423+ },
424+ variables = {"data" : params },
425+ )
259426
260- print (
261- api .graphql_request (
427+ if errors := resp .get ("errors" ):
428+ logger .error ("Failed to create project asset: %s" , errors )
429+ return None
430+
431+ if errors := resp ["data" ]["createProjectAsset" ].get ("messages" ):
432+ logger .error ("Failed to create project asset: %s" , errors )
433+ return None
434+
435+ if errors := resp ["data" ]["createProjectAsset" ].get ("errors" ):
436+ logger .error ("Failed to create project asset: %s" , errors )
437+ return None
438+
439+ return resp ["data" ]["createProjectAsset" ]["result" ]["id" ]
440+
441+
442+ with MapSwipeApiClient () as api_client :
443+ logger .info ("Public endpoints" )
444+
445+ logger .info (
446+ "%s: %s" ,
447+ Query .PUBLIC_PROJECTS_OP_NAME ,
448+ api_client .graphql_request (
449+ Query .PUBLIC_PROJECTS_OP_NAME ,
262450 Query .PUBLIC_PROJECTS ,
263451 variables = {
264452 "filters" : {
@@ -267,10 +455,57 @@ def graphql_request(self, query, variables = None):
267455 }
268456 }
269457 },
270- )
458+ ),
459+ )
460+
461+ logger .info ("Private endpoints" )
462+ me_info = api_client .graphql_request (Query .ME_OP_NAME , Query .ME )["data" ]["me" ]
463+ if not me_info :
464+ raise Exception ("Not logged in.... :(" )
465+ logger .info ("%s: %s" , Query .ME_OP_NAME , me_info )
466+
467+ organization_id = api_client .graphql_request (
468+ Query .ORGANIZATIONS_OP_NAME ,
469+ Query .ORGANIZATIONS ,
470+ )["data" ]["organizations" ]["results" ][0 ]["id" ]
471+
472+ logger .info (
473+ "%s: %s" ,
474+ Query .PUBLIC_PROJECTS_OP_NAME ,
475+ api_client .graphql_request (Query .PROJECTS_OP_NAME , Query .PROJECTS ),
271476 )
272477
273- print ('Private endpoints' )
274- print (api .graphql_request (Query .ME ))
478+ new_project_client_id = str (ULID ())
479+ new_project_topic_name = "Test - Building Validation - 8"
480+
481+ new_project_id = api_client .create_draft_project (
482+ {
483+ "clientId" : new_project_client_id ,
484+ "projectType" : "VALIDATE" ,
485+ "region" : "Nepal" ,
486+ "topic" : new_project_topic_name ,
487+ "description" : "Validate building footprints" ,
488+ "projectInstruction" : "Validate building footprints" ,
489+ "lookFor" : "buildings" ,
490+ "projectNumber" : 1000 ,
491+ "requestingOrganization" : organization_id ,
492+ "additionalInfoUrl" : "fair-dev.hotosm.org" ,
493+ "team" : None ,
494+ }
495+ )
496+ assert new_project_id is not None
497+
498+ logger .info ("%s: %s" , "Create Draft Project" , new_project_id )
499+
500+ with open ("./sample_image.png" , "rb" ) as image_file :
501+ new_project_asset_client_id = str (ULID ())
502+ new_project_asset = api_client .create_project_asset (
503+ project_file = image_file ,
504+ params = {
505+ "inputType" : "COVER_IMAGE" ,
506+ "clientId" : new_project_asset_client_id ,
507+ "project" : new_project_id ,
508+ },
509+ )
275510
276- print ( api . graphql_request ( Query . PROJECTS ) )
511+ logger . info ( "%s: %s" , "Create Project Asset" , new_project_asset )
0 commit comments