aletrn commited on
Commit
0f9c611
1 Parent(s): c76cb5f

[refactor] first working stage for huggingface docker container with fastapi and loguru

Browse files
.idea/misc.xml CHANGED
@@ -3,5 +3,5 @@
3
  <component name="Black">
4
  <option name="sdkName" value="Python 3.11 (samgis)" />
5
  </component>
6
- <component name="ProjectRootManager" version="2" project-jdk-name="Pipenv (samgis)" project-jdk-type="Python SDK" />
7
  </project>
 
3
  <component name="Black">
4
  <option name="sdkName" value="Python 3.11 (samgis)" />
5
  </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="Poetry (samgis)" project-jdk-type="Python SDK" />
7
  </project>
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM localhost/samgis-base-fastapi:latest
2
+
3
+ # Include global arg in this stage of the build
4
+ ARG LAMBDA_TASK_ROOT="/var/task"
5
+ ARG PYTHONPATH="${LAMBDA_TASK_ROOT}:${PYTHONPATH}:/usr/local/lib/python3/dist-packages"
6
+ ENV VIRTUAL_ENV=${LAMBDA_TASK_ROOT}/.venv \
7
+ PATH="${LAMBDA_TASK_ROOT}/.venv/bin:$PATH"
8
+
9
+ # Set working directory to function root directory
10
+ WORKDIR ${LAMBDA_TASK_ROOT}
11
+ COPY ./samgis ${LAMBDA_TASK_ROOT}/samgis
12
+ COPY ./src ${LAMBDA_TASK_ROOT}/src
13
+ COPY ./static ${LAMBDA_TASK_ROOT}/static
14
+ COPY ./machine_learning_models ${LAMBDA_TASK_ROOT}/machine_learning_models
15
+
16
+ RUN ls -l /usr/bin/which
17
+ RUN /usr/bin/which python
18
+ RUN python -v
19
+ RUN echo "PYTHONPATH: ${PYTHONPATH}."
20
+ RUN echo "PATH: ${PATH}."
21
+ RUN echo "LAMBDA_TASK_ROOT: ${LAMBDA_TASK_ROOT}."
22
+ RUN ls -l ${LAMBDA_TASK_ROOT}
23
+ RUN ls -ld ${LAMBDA_TASK_ROOT}
24
+ RUN ls -l ${LAMBDA_TASK_ROOT}/machine_learning_models
25
+ RUN python -c "import sys; print(sys.path)"
26
+ RUN python -c "import cv2"
27
+ RUN python -c "import fastapi"
28
+ RUN python -c "import geopandas"
29
+ RUN python -c "import loguru"
30
+ RUN python -c "import onnxruntime"
31
+ RUN python -c "import rasterio"
32
+ RUN python -c "import uvicorn"
33
+ RUN df -h
34
+ RUN ls -l ${LAMBDA_TASK_ROOT}/samgis/
35
+ RUN ls -l ${LAMBDA_TASK_ROOT}/src/
36
+ RUN ls -l ${LAMBDA_TASK_ROOT}/static/
37
+
38
+ CMD ["uvicorn", "src.fastapi_wrapper:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,36 +1,64 @@
1
- # SamGIS
 
 
 
 
 
 
 
2
 
3
- ## todo
4
 
5
- 1. export output to mask: OK local, OK aws lambda
6
- 2. resolve model paths: OK local
7
- 3. inference:
8
- 4. from mask to json (rasterio + geopandas, check for re-projection to EPSG_4326)
9
- 5. check mandatory dependencies
10
- 6. check for alternative python interpreters
11
 
12
- ## Build instructions
 
 
 
 
 
13
 
14
- Build the docker image:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  ```bash
17
  # clean any old active containers
18
  docker stop $(docker ps -a -q); docker rm $(docker ps -a -q)
19
 
20
  # build the base docker image with the docker aws repository tag
21
- docker build . -f dockerfiles/dockerfile-lambda-gdal-runner --tag 686901913580.dkr.ecr.eu-west-1.amazonaws.com/lambda-gdal-runner
22
-
23
- # OPTIONAL: to build the lambda-gdal-runner image on a x86 machine use the build arg `RIE="https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie"`:
24
- docker build . -f dockerfiles/dockerfile-lambda-gdal-runner --build-arg RIE="https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie" --tag 686901913580.dkr.ecr.eu-west-1.amazonaws.com/lambda-gdal-runner --progress=plain
25
 
26
  # build the final docker image
27
- docker build . -f dockerfiles/dockerfile-lambda-fastsam-api --tag 686901913580.dkr.ecr.eu-west-1.amazonaws.com/lambda-fastsam-api
28
  ```
29
 
30
  Run the container (keep it on background) and show logs
31
 
32
  ```bash
33
- docker tag 686901913580.dkr.ecr.eu-west-1.amazonaws.com/lambda-fastsam-api:latest lambda-fastsam-api;docker run -d --name lambda-fastsam-api -p 8080:8080 lambda-fastsam-api; docker logs -f lambda-fastsam-api
34
  ```
35
 
36
  Test it with curl:
@@ -43,25 +71,15 @@ curl -X 'POST' \
43
  -d '{}'
44
  ```
45
 
46
- ## Publish the aws lambda
47
- 1. Login on aws ECR with the correct aws profile (details on [ECR page](https://eu-west-1.console.aws.amazon.com/ecr/repositories/private/686901913580/surferdtm-prediction-api?region=eu-west-1))
48
- ```
49
- aws --profile alessandrotrinca_hotmail_aws_console_ec2_lambda ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 686901913580.dkr.ecr.eu-west-1.amazonaws.com
50
- ```
51
- 2. Build and tag the docker images, then push them:
52
- ```
53
- docker push 686901913580.dkr.ecr.eu-west-1.amazonaws.com/lambda-gdal-runner:latest
54
- docker push 686901913580.dkr.ecr.eu-west-1.amazonaws.com/lambda-fastsam-api:latest
55
- ```
56
- 3. It's possible to publish a new aws lambda version from cmd or from lambda page
57
-
58
-
59
- ## Dependencies installation and local tests
60
- The docker build process needs only the classic requirements.txt (here renamed to `requirements_dockerfile.txt`), instead for local development and sphinx-docs build
61
- there is `Pipfile` (sphinx docs is hosted on Cloudflare Pages).
62
 
 
 
 
63
 
64
- ## Tests
65
 
66
  Tests are defined in the `tests` folder in this project. Use PIP to install the test dependencies and run tests.
67
 
@@ -69,7 +87,7 @@ Tests are defined in the `tests` folder in this project. Use PIP to install the
69
  python -m pytest --cov=samgis --cov-report=term-missing && coverage html
70
  ```
71
 
72
- ## Update the static documentation with sphinx
73
 
74
  Run the sphinx-apidoc: it's a tool for automatic generation of Sphinx sources that, using the autodoc
75
  extension, document a whole package in the style of other automatic API documentation tools. See the
@@ -78,7 +96,7 @@ Run the command from the project root:
78
 
79
  ```bash
80
  # missing docs folder (run from project root)
81
- cd docs && sphinx-quickstart -p SamGIS -a "alessandro trinca tornidor" -r 1.0.0 -l python --master index
82
 
83
  # update docs folder (from project root)
84
  sphinx-apidoc -f -o docs samgis
 
1
+ ---
2
+ title: SamGIS
3
+ emoji: 📉
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
 
10
+ ## SamGIS - HuggingFace version
11
 
12
+ Build the docker image this way:
 
 
 
 
 
13
 
14
+ ```bash
15
+ # clean any old active containers
16
+ docker stop $(docker ps -a -q); docker rm $(docker ps -a -q)
17
+
18
+ # build the base docker image with the ARG DEPENDENCY_GROUP=fastapi used by poetry
19
+ docker build . -f dockerfiles/dockerfile-samgis-base --build-arg DEPENDENCY_GROUP=fastapi --tag localhost/samgis-base-fastapi --progress=plain
20
 
21
+ # build the image, use the tag "samgis-huggingface"
22
+ docker build . --tag samgis-huggingface --progress=plain
23
+ ```
24
+
25
+ Run the container (keep it on background) and show logs
26
+
27
+ ```bash
28
+ docker run -d --name samgis-huggingface -p 7860:7860 samgis-huggingface; docker logs -f samgis-huggingface
29
+ ```
30
+
31
+ Test it with curl:
32
+
33
+ ```bash
34
+ curl -X 'POST' \
35
+ 'http://localhost:7860/infer_samgeo' \
36
+ -H 'accept: application/json' \
37
+ -d '{}'
38
+ ```
39
+
40
+ or better visiting the swagger page on http://localhost:7860/docs
41
+
42
+
43
+ ## SamGIS - lambda AWS version
44
+
45
+ Build the docker image this way:
46
 
47
  ```bash
48
  # clean any old active containers
49
  docker stop $(docker ps -a -q); docker rm $(docker ps -a -q)
50
 
51
  # build the base docker image with the docker aws repository tag
52
+ docker build . -f dockerfiles/dockerfile-samgis-base --build-arg DEPENDENCY_GROUP=aws_lambda --tag localhost/samgis-base-aws-lambda --progress=plain
 
 
 
53
 
54
  # build the final docker image
55
+ docker build . -f dockerfiles/dockerfile-lambda-fastsam-api --tag localhost/lambda-fastsam-api --progress=plain
56
  ```
57
 
58
  Run the container (keep it on background) and show logs
59
 
60
  ```bash
61
+ docker tag localhost/lambda-fastsam-api:latest localhost/lambda-fastsam-api;docker run -d --name lambda-fastsam-api -p 8080:8080 lambda-fastsam-api; docker logs -f lambda-fastsam-api
62
  ```
63
 
64
  Test it with curl:
 
71
  -d '{}'
72
  ```
73
 
74
+ ### Publish the aws lambda docker image
75
+ Login on aws ECR with the correct aws profile (change the example `localhost/` repository url with the one from
76
+ the [ECR push command instructions page](https://eu-west-1.console.aws.amazon.com/ecr/repositories/)).
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
+ ### Dependencies installation and local tests
79
+ The docker build process needs only the base dependency group plus the `aws_lambda` or `fastapi` optional one.
80
+ Install also the `test` and/or `docs` groups if needed.
81
 
82
+ ### Tests
83
 
84
  Tests are defined in the `tests` folder in this project. Use PIP to install the test dependencies and run tests.
85
 
 
87
  python -m pytest --cov=samgis --cov-report=term-missing && coverage html
88
  ```
89
 
90
+ ### How to update the static documentation with sphinx
91
 
92
  Run the sphinx-apidoc: it's a tool for automatic generation of Sphinx sources that, using the autodoc
93
  extension, document a whole package in the style of other automatic API documentation tools. See the
 
96
 
97
  ```bash
98
  # missing docs folder (run from project root)
99
+ cd docs && sphinx-quickstart -p SamGIS -r 1.0.0 -l python --master index
100
 
101
  # update docs folder (from project root)
102
  sphinx-apidoc -f -o docs samgis
dockerfiles/dockerfile-fastapi-fastsam-api ADDED
@@ -0,0 +1 @@
 
 
1
+ Dockerfile
dockerfiles/dockerfile-lambda-fastsam-api CHANGED
@@ -1,4 +1,4 @@
1
- FROM 686901913580.dkr.ecr.eu-west-1.amazonaws.com/lambda-gdal-runner:latest
2
 
3
  # Include global arg in this stage of the build
4
  ARG LAMBDA_TASK_ROOT="/var/task"
@@ -22,15 +22,15 @@ RUN ls -l ${LAMBDA_TASK_ROOT}
22
  RUN ls -ld ${LAMBDA_TASK_ROOT}
23
  RUN ls -l ${LAMBDA_TASK_ROOT}/machine_learning_models
24
  RUN python -c "import sys; print(sys.path)"
 
 
25
  RUN python -c "import geopandas"
26
  RUN python -c "import onnxruntime"
27
  RUN python -c "import rasterio"
28
- RUN python -c "import awslambdaric"
29
- RUN python -c "import cv2"
30
  RUN df -h
31
  RUN ls -l /lambda-entrypoint.sh
32
  RUN ls -l ${LAMBDA_TASK_ROOT}/samgis/
33
  RUN ls -l ${LAMBDA_TASK_ROOT}/src/
34
 
35
- # ENTRYPOINT ["/lambda-entrypoint.sh"]
36
  CMD [ "src.lambda_wrapper.lambda_handler" ]
 
1
+ FROM localhost/samgis-base-aws-lambda:latest
2
 
3
  # Include global arg in this stage of the build
4
  ARG LAMBDA_TASK_ROOT="/var/task"
 
22
  RUN ls -ld ${LAMBDA_TASK_ROOT}
23
  RUN ls -l ${LAMBDA_TASK_ROOT}/machine_learning_models
24
  RUN python -c "import sys; print(sys.path)"
25
+ RUN python -c "import awslambdaric"
26
+ RUN python -c "import cv2"
27
  RUN python -c "import geopandas"
28
  RUN python -c "import onnxruntime"
29
  RUN python -c "import rasterio"
 
 
30
  RUN df -h
31
  RUN ls -l /lambda-entrypoint.sh
32
  RUN ls -l ${LAMBDA_TASK_ROOT}/samgis/
33
  RUN ls -l ${LAMBDA_TASK_ROOT}/src/
34
 
35
+ ENTRYPOINT ["/lambda-entrypoint.sh"]
36
  CMD [ "src.lambda_wrapper.lambda_handler" ]
dockerfiles/{dockerfile-lambda-gdal-runner → dockerfile-samgis-base} RENAMED
@@ -19,12 +19,15 @@ ARG POETRY_VIRTUALENVS_IN_PROJECT
19
  ARG POETRY_VIRTUALENVS_CREATE
20
  ARG POETRY_CACHE_DIR
21
  ARG RIE
 
22
 
23
  RUN echo "ARCH: $ARCH ..."
24
 
25
  RUN echo "ARG RIE: $RIE ..."
26
  RUN echo "ARG POETRY_CACHE_DIR: ${POETRY_CACHE_DIR} ..."
27
  RUN echo "ARG PYTHONPATH: $PYTHONPATH ..."
 
 
28
 
29
  # Set working directory to function root directory
30
  WORKDIR ${LAMBDA_TASK_ROOT}
@@ -44,7 +47,7 @@ RUN python -m pip install -r ${LAMBDA_TASK_ROOT}/requirements_poetry.txt
44
  RUN which poetry && poetry --version && poetry config --list
45
  RUN poetry config virtualenvs.path ${LAMBDA_TASK_ROOT}
46
  RUN echo "# poetry config --list #" && poetry config --list
47
- RUN poetry install --with aws_lambda --no-root
48
 
49
  RUN curl -Lo /usr/local/bin/aws-lambda-rie ${RIE}
50
 
@@ -62,7 +65,6 @@ ARG LAMBDA_TASK_ROOT
62
  ENV VIRTUAL_ENV=${LAMBDA_TASK_ROOT}/.venv \
63
  PATH="${LAMBDA_TASK_ROOT}/.venv/bin:$PATH"
64
 
65
- COPY --from=builder /usr/local/bin/aws-lambda-rie /usr/local/bin/aws-lambda-rie
66
  RUN echo "COPY --from=builder /usr/lib/${ARCH}-linux-gnu/libGL.so* /usr/lib/${ARCH}-linux-gnu/"
67
  COPY --from=builder /usr/lib/${ARCH}-linux-gnu/libGL.so* /usr/lib/${ARCH}-linux-gnu/
68
  COPY --from=builder ${LAMBDA_TASK_ROOT}/.venv ${LAMBDA_TASK_ROOT}/.venv
@@ -71,10 +73,9 @@ RUN echo "new LAMBDA_TASK_ROOT after hidden venv copy => ${LAMBDA_TASK_ROOT}"
71
  RUN ls -ld ${LAMBDA_TASK_ROOT}/
72
  RUN ls -lA ${LAMBDA_TASK_ROOT}/
73
 
 
 
74
  RUN chmod +x /usr/local/bin/aws-lambda-rie
75
-
76
  COPY ./scripts/lambda-entrypoint.sh /lambda-entrypoint.sh
77
  RUN chmod +x /lambda-entrypoint.sh
78
  RUN ls -l /lambda-entrypoint.sh
79
-
80
- ENTRYPOINT ["/lambda-entrypoint.sh"]
 
19
  ARG POETRY_VIRTUALENVS_CREATE
20
  ARG POETRY_CACHE_DIR
21
  ARG RIE
22
+ ARG DEPENDENCY_GROUP
23
 
24
  RUN echo "ARCH: $ARCH ..."
25
 
26
  RUN echo "ARG RIE: $RIE ..."
27
  RUN echo "ARG POETRY_CACHE_DIR: ${POETRY_CACHE_DIR} ..."
28
  RUN echo "ARG PYTHONPATH: $PYTHONPATH ..."
29
+ RUN test -n ${DEPENDENCY_GROUP:?}
30
+ RUN echo "python DEPENDENCY_GROUP: ${DEPENDENCY_GROUP} ..."
31
 
32
  # Set working directory to function root directory
33
  WORKDIR ${LAMBDA_TASK_ROOT}
 
47
  RUN which poetry && poetry --version && poetry config --list
48
  RUN poetry config virtualenvs.path ${LAMBDA_TASK_ROOT}
49
  RUN echo "# poetry config --list #" && poetry config --list
50
+ RUN poetry install --with ${DEPENDENCY_GROUP} --no-root
51
 
52
  RUN curl -Lo /usr/local/bin/aws-lambda-rie ${RIE}
53
 
 
65
  ENV VIRTUAL_ENV=${LAMBDA_TASK_ROOT}/.venv \
66
  PATH="${LAMBDA_TASK_ROOT}/.venv/bin:$PATH"
67
 
 
68
  RUN echo "COPY --from=builder /usr/lib/${ARCH}-linux-gnu/libGL.so* /usr/lib/${ARCH}-linux-gnu/"
69
  COPY --from=builder /usr/lib/${ARCH}-linux-gnu/libGL.so* /usr/lib/${ARCH}-linux-gnu/
70
  COPY --from=builder ${LAMBDA_TASK_ROOT}/.venv ${LAMBDA_TASK_ROOT}/.venv
 
73
  RUN ls -ld ${LAMBDA_TASK_ROOT}/
74
  RUN ls -lA ${LAMBDA_TASK_ROOT}/
75
 
76
+ # only used by AWS lambda docker image
77
+ COPY --from=builder /usr/local/bin/aws-lambda-rie /usr/local/bin/aws-lambda-rie
78
  RUN chmod +x /usr/local/bin/aws-lambda-rie
 
79
  COPY ./scripts/lambda-entrypoint.sh /lambda-entrypoint.sh
80
  RUN chmod +x /lambda-entrypoint.sh
81
  RUN ls -l /lambda-entrypoint.sh
 
 
events/payload_point.json CHANGED
@@ -9,6 +9,6 @@
9
  "label": 0
10
  }],
11
  "zoom": 6,
12
- "source_type": "Satellite",
13
  "debug": true
14
  }
 
9
  "label": 0
10
  }],
11
  "zoom": 6,
12
+ "source_type": "OpenStreetMap",
13
  "debug": true
14
  }
events/payload_point2.json CHANGED
@@ -9,6 +9,6 @@
9
  "label": 0
10
  }],
11
  "zoom": 10,
12
- "source_type": "Satellite",
13
  "debug": true
14
  }
 
9
  "label": 0
10
  }],
11
  "zoom": 10,
12
+ "source_type": "OpenStreetMap",
13
  "debug": true
14
  }
events/payload_point_eolie.json CHANGED
@@ -20,5 +20,5 @@
20
  }
21
  ],
22
  "zoom": 10,
23
- "source_type": "Satellite"
24
  }
 
20
  }
21
  ],
22
  "zoom": 10,
23
+ "source_type": "OpenStreetMap"
24
  }
poetry.lock CHANGED
@@ -37,6 +37,26 @@ files = [
37
  {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
38
  ]
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  [[package]]
41
  name = "attrs"
42
  version = "23.1.0"
@@ -512,6 +532,25 @@ files = [
512
  {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"},
513
  ]
514
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  [[package]]
516
  name = "fiona"
517
  version = "1.9.5"
@@ -688,6 +727,17 @@ dev-test = ["coverage", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "sphinx (<
688
  requests = ["requests (>=2.16.2)", "urllib3 (>=1.24.2)"]
689
  timezone = ["pytz"]
690
 
 
 
 
 
 
 
 
 
 
 
 
691
  [[package]]
692
  name = "humanfriendly"
693
  version = "10.0"
@@ -922,6 +972,24 @@ files = [
922
  {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"},
923
  ]
924
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
  [[package]]
926
  name = "markdown-it-py"
927
  version = "3.0.0"
@@ -2143,6 +2211,17 @@ files = [
2143
  {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
2144
  ]
2145
 
 
 
 
 
 
 
 
 
 
 
 
2146
  [[package]]
2147
  name = "snowballstemmer"
2148
  version = "2.2.0"
@@ -2380,6 +2459,23 @@ Sphinx = ">=5"
2380
  lint = ["docutils-stubs", "flake8", "mypy"]
2381
  test = ["pytest"]
2382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2383
  [[package]]
2384
  name = "sympy"
2385
  version = "1.12"
@@ -2432,6 +2528,38 @@ brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
2432
  socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
2433
  zstd = ["zstandard (>=0.18.0)"]
2434
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2435
  [[package]]
2436
  name = "xyzservices"
2437
  version = "2023.10.1"
@@ -2446,4 +2574,4 @@ files = [
2446
  [metadata]
2447
  lock-version = "2.0"
2448
  python-versions = "^3.11"
2449
- content-hash = "02681a973e50683328473fff27fa817d69380d075831de85da66fb238fe90866"
 
37
  {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
38
  ]
39
 
40
+ [[package]]
41
+ name = "anyio"
42
+ version = "4.2.0"
43
+ description = "High level compatibility layer for multiple asynchronous event loop implementations"
44
+ optional = false
45
+ python-versions = ">=3.8"
46
+ files = [
47
+ {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"},
48
+ {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"},
49
+ ]
50
+
51
+ [package.dependencies]
52
+ idna = ">=2.8"
53
+ sniffio = ">=1.1"
54
+
55
+ [package.extras]
56
+ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
57
+ test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
58
+ trio = ["trio (>=0.23)"]
59
+
60
  [[package]]
61
  name = "attrs"
62
  version = "23.1.0"
 
532
  {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"},
533
  ]
534
 
535
+ [[package]]
536
+ name = "fastapi"
537
+ version = "0.108.0"
538
+ description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
539
+ optional = false
540
+ python-versions = ">=3.8"
541
+ files = [
542
+ {file = "fastapi-0.108.0-py3-none-any.whl", hash = "sha256:8c7bc6d315da963ee4cdb605557827071a9a7f95aeb8fcdd3bde48cdc8764dd7"},
543
+ {file = "fastapi-0.108.0.tar.gz", hash = "sha256:5056e504ac6395bf68493d71fcfc5352fdbd5fda6f88c21f6420d80d81163296"},
544
+ ]
545
+
546
+ [package.dependencies]
547
+ pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
548
+ starlette = ">=0.29.0,<0.33.0"
549
+ typing-extensions = ">=4.8.0"
550
+
551
+ [package.extras]
552
+ all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
553
+
554
  [[package]]
555
  name = "fiona"
556
  version = "1.9.5"
 
727
  requests = ["requests (>=2.16.2)", "urllib3 (>=1.24.2)"]
728
  timezone = ["pytz"]
729
 
730
+ [[package]]
731
+ name = "h11"
732
+ version = "0.14.0"
733
+ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
734
+ optional = false
735
+ python-versions = ">=3.7"
736
+ files = [
737
+ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
738
+ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
739
+ ]
740
+
741
  [[package]]
742
  name = "humanfriendly"
743
  version = "10.0"
 
972
  {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"},
973
  ]
974
 
975
+ [[package]]
976
+ name = "loguru"
977
+ version = "0.7.2"
978
+ description = "Python logging made (stupidly) simple"
979
+ optional = false
980
+ python-versions = ">=3.5"
981
+ files = [
982
+ {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"},
983
+ {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"},
984
+ ]
985
+
986
+ [package.dependencies]
987
+ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
988
+ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
989
+
990
+ [package.extras]
991
+ dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"]
992
+
993
  [[package]]
994
  name = "markdown-it-py"
995
  version = "3.0.0"
 
2211
  {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
2212
  ]
2213
 
2214
+ [[package]]
2215
+ name = "sniffio"
2216
+ version = "1.3.0"
2217
+ description = "Sniff out which async library your code is running under"
2218
+ optional = false
2219
+ python-versions = ">=3.7"
2220
+ files = [
2221
+ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
2222
+ {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
2223
+ ]
2224
+
2225
  [[package]]
2226
  name = "snowballstemmer"
2227
  version = "2.2.0"
 
2459
  lint = ["docutils-stubs", "flake8", "mypy"]
2460
  test = ["pytest"]
2461
 
2462
+ [[package]]
2463
+ name = "starlette"
2464
+ version = "0.32.0.post1"
2465
+ description = "The little ASGI library that shines."
2466
+ optional = false
2467
+ python-versions = ">=3.8"
2468
+ files = [
2469
+ {file = "starlette-0.32.0.post1-py3-none-any.whl", hash = "sha256:cd0cb10ddb49313f609cedfac62c8c12e56c7314b66d89bb077ba228bada1b09"},
2470
+ {file = "starlette-0.32.0.post1.tar.gz", hash = "sha256:e54e2b7e2fb06dff9eac40133583f10dfa05913f5a85bf26f427c7a40a9a3d02"},
2471
+ ]
2472
+
2473
+ [package.dependencies]
2474
+ anyio = ">=3.4.0,<5"
2475
+
2476
+ [package.extras]
2477
+ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
2478
+
2479
  [[package]]
2480
  name = "sympy"
2481
  version = "1.12"
 
2528
  socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
2529
  zstd = ["zstandard (>=0.18.0)"]
2530
 
2531
+ [[package]]
2532
+ name = "uvicorn"
2533
+ version = "0.25.0"
2534
+ description = "The lightning-fast ASGI server."
2535
+ optional = false
2536
+ python-versions = ">=3.8"
2537
+ files = [
2538
+ {file = "uvicorn-0.25.0-py3-none-any.whl", hash = "sha256:ce107f5d9bd02b4636001a77a4e74aab5e1e2b146868ebbad565237145af444c"},
2539
+ {file = "uvicorn-0.25.0.tar.gz", hash = "sha256:6dddbad1d7ee0f5140aba5ec138ddc9612c5109399903828b4874c9937f009c2"},
2540
+ ]
2541
+
2542
+ [package.dependencies]
2543
+ click = ">=7.0"
2544
+ h11 = ">=0.8"
2545
+
2546
+ [package.extras]
2547
+ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
2548
+
2549
+ [[package]]
2550
+ name = "win32-setctime"
2551
+ version = "1.1.0"
2552
+ description = "A small Python utility to set file creation time on Windows"
2553
+ optional = false
2554
+ python-versions = ">=3.5"
2555
+ files = [
2556
+ {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
2557
+ {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
2558
+ ]
2559
+
2560
+ [package.extras]
2561
+ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
2562
+
2563
  [[package]]
2564
  name = "xyzservices"
2565
  version = "2023.10.1"
 
2574
  [metadata]
2575
  lock-version = "2.0"
2576
  python-versions = "^3.11"
2577
+ content-hash = "61953101662fbd1d752c6beb1b60c5ee803794c4ecb9f2484bdc28c884cd077e"
pyproject.toml CHANGED
@@ -18,6 +18,7 @@ python-dotenv = "^1.0.0"
18
  rasterio = "^1.3.9"
19
  requests = "^2.31.0"
20
  pillow = "^10.1.0"
 
21
 
22
  [tool.poetry.group.aws_lambda]
23
  optional = true
@@ -26,7 +27,7 @@ optional = true
26
  aws-lambda-powertools = "^2.30.2"
27
  awslambdaric = "^2.0.8"
28
  jmespath = "^1.0.1"
29
- pydantic = ">=2.0.3"
30
 
31
  [tool.poetry.group.test]
32
  optional = true
@@ -45,6 +46,15 @@ sphinx-autodoc-typehints = "^1.25.2"
45
  sphinxcontrib-openapi = "^0.8.3"
46
  myst-parser = "^2.0.0"
47
 
 
 
 
 
 
 
 
 
 
48
  [build-system]
49
  requires = ["poetry-core"]
50
  build-backend = "poetry.core.masonry.api"
 
18
  rasterio = "^1.3.9"
19
  requests = "^2.31.0"
20
  pillow = "^10.1.0"
21
+ loguru = "^0.7.2"
22
 
23
  [tool.poetry.group.aws_lambda]
24
  optional = true
 
27
  aws-lambda-powertools = "^2.30.2"
28
  awslambdaric = "^2.0.8"
29
  jmespath = "^1.0.1"
30
+ pydantic = "^2.5.3"
31
 
32
  [tool.poetry.group.test]
33
  optional = true
 
46
  sphinxcontrib-openapi = "^0.8.3"
47
  myst-parser = "^2.0.0"
48
 
49
+ [tool.poetry.group.fastapi]
50
+ optional = true
51
+
52
+ [tool.poetry.group.fastapi.dependencies]
53
+ fastapi = "^0.108.0"
54
+ loguru = "^0.7.2"
55
+ pydantic = "^2.5.3"
56
+ uvicorn = "^0.25.0"
57
+
58
  [build-system]
59
  requires = ["poetry-core"]
60
  build-backend = "poetry.core.masonry.api"
samgis/__init__.py CHANGED
@@ -1,5 +1,4 @@
1
  """Get machine learning predictions from geodata raster images"""
2
- from aws_lambda_powertools import Logger
3
  # not used here but contextily_tile is imported in samgis.io.tms2geotiff
4
  from contextily import tile as contextily_tile
5
  from pathlib import Path
@@ -9,4 +8,34 @@ from samgis.utilities.constants import SERVICE_NAME
9
 
10
  PROJECT_ROOT_FOLDER = Path(globals().get("__file__", "./_")).absolute().parent.parent
11
  MODEL_FOLDER = Path(PROJECT_ROOT_FOLDER / "machine_learning_models")
12
- app_logger = Logger(service=SERVICE_NAME)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Get machine learning predictions from geodata raster images"""
 
2
  # not used here but contextily_tile is imported in samgis.io.tms2geotiff
3
  from contextily import tile as contextily_tile
4
  from pathlib import Path
 
8
 
9
  PROJECT_ROOT_FOLDER = Path(globals().get("__file__", "./_")).absolute().parent.parent
10
  MODEL_FOLDER = Path(PROJECT_ROOT_FOLDER / "machine_learning_models")
11
+ try:
12
+ from aws_lambda_powertools import Logger
13
+
14
+ app_logger = Logger(service=SERVICE_NAME)
15
+ except ModuleNotFoundError:
16
+ import loguru
17
+
18
+ def setup_logging(debug: bool = False, formatter: str = "{time} - {level} - ({extra[request_id]}) {message} "
19
+ ) -> loguru.logger:
20
+ """
21
+ Create a logging instance with log string formatter.
22
+
23
+ Args:
24
+ debug: logging debug argument
25
+ formatter: log string formatter
26
+
27
+ Returns:
28
+ Logger
29
+
30
+ """
31
+ import sys
32
+
33
+ logger = loguru.logger
34
+ logger.remove()
35
+ level_logger = "DEBUG" if debug else "INFO"
36
+ logger.add(sys.stdout, format=formatter, level=level_logger)
37
+ logger.info(f"type_logger:{type(logger)}, logger:{logger}.")
38
+ return logger
39
+
40
+
41
+ app_logger = setup_logging(debug=True)
samgis/io/lambda_helpers.py CHANGED
@@ -1,12 +1,11 @@
1
  """lambda helper functions"""
2
  from typing import Dict
3
  from xyzservices import providers
4
- from aws_lambda_powertools.event_handler import content_types
5
 
6
  from samgis import app_logger
7
  from samgis.io.coordinates_pixel_conversion import get_latlng_to_pixel_coordinates
8
  from samgis.utilities.constants import CUSTOM_RESPONSE_MESSAGES
9
- from samgis.utilities.type_hints import ApiRequestBody
10
  from samgis.utilities.utilities import base64_decode
11
 
12
 
@@ -34,7 +33,7 @@ def get_response(status: int, start_time: float, request_id: str, response_body:
34
 
35
  response = {
36
  "statusCode": status,
37
- "header": {"Content-Type": content_types.APPLICATION_JSON},
38
  "body": dumps(response_body),
39
  "isBase64Encoded": False
40
  }
 
1
  """lambda helper functions"""
2
  from typing import Dict
3
  from xyzservices import providers
 
4
 
5
  from samgis import app_logger
6
  from samgis.io.coordinates_pixel_conversion import get_latlng_to_pixel_coordinates
7
  from samgis.utilities.constants import CUSTOM_RESPONSE_MESSAGES
8
+ from samgis.utilities.type_hints import ApiRequestBody, ContentTypes
9
  from samgis.utilities.utilities import base64_decode
10
 
11
 
 
33
 
34
  response = {
35
  "statusCode": status,
36
+ "header": {"Content-Type": ContentTypes.APPLICATION_JSON},
37
  "body": dumps(response_body),
38
  "isBase64Encoded": False
39
  }
samgis/utilities/type_hints.py CHANGED
@@ -31,6 +31,13 @@ class LatLngDict(BaseModel):
31
  lng: float
32
 
33
 
 
 
 
 
 
 
 
34
  class PromptPointType(str, Enum):
35
  """Segment Anything: validation point prompt type"""
36
  point = "point"
 
31
  lng: float
32
 
33
 
34
+ class ContentTypes(str, Enum):
35
+ """Segment Anything: validation point prompt type"""
36
+ APPLICATION_JSON = "application/json"
37
+ TEXT_PLAIN = "text/plain"
38
+ TEXT_HTML = "text/html"
39
+
40
+
41
  class PromptPointType(str, Enum):
42
  """Segment Anything: validation point prompt type"""
43
  point = "point"
src/fastapi_wrapper.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import uuid
3
+
4
+ from fastapi import FastAPI, HTTPException, Request, status
5
+ from fastapi.exceptions import RequestValidationError
6
+ from fastapi.responses import FileResponse, JSONResponse
7
+ from fastapi.staticfiles import StaticFiles
8
+
9
+ from samgis import app_logger
10
+ from samgis.io.lambda_helpers import get_parsed_bbox_points
11
+ from samgis.utilities.type_hints import ApiRequestBody
12
+
13
+ app = FastAPI()
14
+
15
+
16
+ @app.middleware("http")
17
+ async def request_middleware(request, call_next):
18
+ request_id = str(uuid.uuid4())
19
+ with app_logger.contextualize(request_id=request_id):
20
+ app_logger.info("Request started")
21
+
22
+ try:
23
+ response = await call_next(request)
24
+
25
+ except Exception as ex:
26
+ app_logger.error(f"Request failed: {ex}")
27
+ response = JSONResponse(content={"success": False}, status_code=500)
28
+
29
+ finally:
30
+ response.headers["X-Request-ID"] = request_id
31
+ app_logger.info(f"Request ended")
32
+ return response
33
+
34
+
35
+ @app.post("/post_test")
36
+ async def post_test(request_input: ApiRequestBody) -> JSONResponse:
37
+ request_body = get_parsed_bbox_points(request_input)
38
+ app_logger.info(f"request_body:{request_body}.")
39
+ return JSONResponse(
40
+ status_code=200,
41
+ content=get_parsed_bbox_points(request_input)
42
+ )
43
+
44
+
45
+ @app.get("/hello")
46
+ async def hello() -> JSONResponse:
47
+ app_logger.info(f"hello")
48
+ return JSONResponse(status_code=200, content={"msg": "hello"})
49
+
50
+
51
+ @app.post("/infer_samgis")
52
+ def samgis(request_input: ApiRequestBody):
53
+ import subprocess
54
+
55
+ from samgis.prediction_api.predictors import samexporter_predict
56
+ app_logger.info("starting inference request...")
57
+
58
+ try:
59
+ import time
60
+
61
+ time_start_run = time.time()
62
+ body_request = get_parsed_bbox_points(request_input)
63
+ app_logger.info(f"body_request:{body_request}.")
64
+ try:
65
+ output = samexporter_predict(
66
+ bbox=body_request["bbox"], prompt=body_request["prompt"], zoom=body_request["zoom"],
67
+ source=body_request["source"]
68
+ )
69
+ duration_run = time.time() - time_start_run
70
+ app_logger.info(f"duration_run:{duration_run}.")
71
+ body = {
72
+ "duration_run": duration_run,
73
+ "output": output
74
+ }
75
+ return JSONResponse(status_code=200, content={"body": json.dumps(body)})
76
+ except Exception as inference_exception:
77
+ home_content = subprocess.run(
78
+ "ls -l /var/task", shell=True, universal_newlines=True, stdout=subprocess.PIPE
79
+ )
80
+ app_logger.error(f"/home/user ls -l: {home_content.stdout}.")
81
+ app_logger.error(f"inference error:{inference_exception}.")
82
+ return HTTPException(status_code=500, detail="Internal server error on inference")
83
+ except Exception as generic_exception:
84
+ app_logger.error(f"generic error:{generic_exception}.")
85
+ return HTTPException(status_code=500, detail="Generic internal server error")
86
+
87
+
88
+ @app.exception_handler(RequestValidationError)
89
+ async def request_validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
90
+ app_logger.error(f"exception errors: {exc.errors()}.")
91
+ app_logger.error(f"exception body: {exc.body}.")
92
+ headers = request.headers.items()
93
+ app_logger.error(f'request header: {dict(headers)}.' )
94
+ params = request.query_params.items()
95
+ app_logger.error(f'request query params: {dict(params)}.')
96
+ return JSONResponse(
97
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
98
+ content={"msg": "Error - Unprocessable Entity"}
99
+ )
100
+
101
+
102
+ @app.exception_handler(HTTPException)
103
+ async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
104
+ app_logger.error(f"exception: {str(exc)}.")
105
+ headers = request.headers.items()
106
+ app_logger.error(f'request header: {dict(headers)}.' )
107
+ params = request.query_params.items()
108
+ app_logger.error(f'request query params: {dict(params)}.')
109
+ return JSONResponse(
110
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
111
+ content={"msg": "Error - Internal Server Error"}
112
+ )
113
+
114
+
115
+ # important: the index() function and the app.mount MUST be at the end
116
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")
117
+
118
+
119
+ @app.get("/")
120
+ def index() -> FileResponse:
121
+ return FileResponse(path="/app/static/index.html", media_type="text/html")
122
+
static/app.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const jsonBtn = document.getElementById("getJson");
2
+ const apiBtn = document.getElementById("getApi");
3
+ const output = document.getElementById("output");
4
+ const coordsForm = document.getElementById("coords-form");
5
+
6
+ function formData2json(dataId, newObj={}) {
7
+ const formData = new FormData(dataId);
8
+ formData.forEach(function(value, key){
9
+ console.log(`formData2json:: key=${key}, value=${value}.`)
10
+ newObj[key] = value;
11
+ });
12
+ return JSON.stringify(newObj);
13
+ }
14
+
15
+ coordsForm.addEventListener('submit', event => {
16
+ event.preventDefault();
17
+ console.log("coordsForm::", coordsForm, "#")
18
+ const inputJson = formData2json(coordsForm)
19
+ console.log("inputJson", inputJson, "#");
20
+
21
+ fetch('/infer_samgis', {
22
+ method: 'POST', // or 'PUT'
23
+ body: inputJson, // a FormData will automatically set the 'Content-Type',
24
+ headers: {"Content-Type": "application/json"},
25
+ }).then(function (response) {
26
+ return response.json();
27
+ }).then(function (data) {
28
+ console.log("data:", data, "#")
29
+ output.innerHTML = JSON.stringify(data)
30
+ }).catch(function (err) {
31
+ console.log("err:", err, "#")
32
+ output.innerHTML = `err:${JSON.stringify(err)}.`;
33
+ });
34
+ event.preventDefault();
35
+ });
static/index.html ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" />
8
+ <title>Fetch API</title>
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <h1>Fetch API</h1>
13
+ <form id="coords-form">
14
+ <label>
15
+ bbox: x1 <input type="number" id="x-center-form" name="x1" value="-122.1497"/>
16
+ </label>
17
+ <label>
18
+ bbox: x2 <input type="number" id="y-center-form" name="x2" value="37.6311"/>
19
+ </label>
20
+ <label>
21
+ bbox: y1 <input type="number" id="y-center-form" name="y1" value="-122.1203"/>
22
+ </label>
23
+ <label>
24
+ bbox: y2 <input type="number" id="y-center-form" name="y2" value="37.6458"/>
25
+ </label>
26
+ <br/><br/>
27
+ <label>
28
+ x point: <input type="number" id="x-form" name="x" value="-122.1419"/>
29
+ </label>
30
+ <label>
31
+ y point: <input type="number" id="y-form" name="y" value="37.6383"/>
32
+ </label>
33
+ <label>
34
+ zoom: <input type="number" id="zoom" name="y" value="6"/>
35
+ </label>
36
+ <br/><br/>
37
+ <button type="submit" id="submit-btn">submit form</button>
38
+ </form>
39
+ <br><br>
40
+ <div id="output"></div>
41
+ </div>
42
+ <script src="app.js"></script>
43
+ </body>
44
+ </html>