Skip to content

Commit 35984b5

Browse files
committed
docs(backend): include draft project creation and file upload examples
1 parent 6f47153 commit 35984b5

File tree

2 files changed

+255
-20
lines changed

2 files changed

+255
-20
lines changed

examples/mapswipe-backend-api/run.py

Lines changed: 255 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,64 @@
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
1015
import httpx
1116
import logging
17+
import colorlog
1218
from dotenv import dotenv_values
19+
from ulid import ULID
1320

14-
logger = logging.getLogger(__name__)
1521
config = 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
1949
class 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)
20.6 KB
Loading

0 commit comments

Comments
 (0)