Skip to content

Commit

Permalink
feat: multiple improvements (#1)
Browse files Browse the repository at this point in the history
* checking in

* api start

* yaaay

* cleaning

* change coordinates to random

* this config outputs the hex instead of the astext()

* adding post

* yey

* sooo close!

* it's alive!! though why i need the commit...

* pre-determined areas

* fml

* changing folder structure to get dockerfile to run db_setup

* yaaaay had to just delete the image and redownload postgis and everything

* woot woooot

* gotta have at least 2 coordinates

* gps schema get

* baby's readme

* cleanup
  • Loading branch information
Erutis authored Jun 28, 2024
1 parent c2259bf commit 5ea1d5a
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 113 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# GIS App

This is a small project that can generate random coordinate data, store it in a Postgres database, and fetch the data using a Sanic API.
83 changes: 83 additions & 0 deletions app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# By: K Agajanian

# Standard libraries
import json

# External libraries
from sanic import Sanic, response, HTTPResponse
# from sanic.response import json

from sqlalchemy import select, func, text
from sqlalchemy.orm import sessionmaker

# Internal libraries
from db_setup import engine_go_vroom
from db_setup import Trajectory


app = Sanic("gis-app")


@app.route("/")
def get(request):
engine = engine_go_vroom()
Session = sessionmaker(bind=engine)
session = Session()

# TODO: Currently hard-coded to get a single trajectory
q = select(Trajectory, func.ST_AsText(Trajectory.geom).label("geom")).where(
Trajectory.id == 2
) # select
q = text(
"SELECT ST_AsText(geom), feed_item_id FROM gps.trajectory WHERE id = 2"
) # text

with session as s:
trajs = s.execute(q).scalars().one_or_none()

# return response.json(trajs.to_dict()) # for select stmt
return response.json(trajs) # for text stmt


@app.post("/")
def post(request):
engine = engine_go_vroom()
Session = sessionmaker(bind=engine)
session = Session()

gps_data = parse_data(request.body)

try:
with session as s:
s.add_all(gps_data)
s.commit()

return HTTPResponse(status=202)

except Exception as e:
print(e)
return HTTPResponse(status=400)


def parse_data(data):
data = json.loads(data)
feed_item_id = data["feed_item_id"]
del data["feed_item_id"]

data = data["gps_data"]

# Create rows from sample data
gps_data = [
Trajectory(
geom=f"SRID=4326;LINESTRINGZM({data})",
feed_item_id=feed_item_id,
)
]
return gps_data


if __name__ == "__main__":
app.run()
105 changes: 48 additions & 57 deletions db_setup.py → app/db_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,86 +2,82 @@
# -*- coding: utf-8 -*-
#
# By: K Agajanian
# Created: 2/21/24

# Standard libraries
from datetime import datetime, timezone

import uuid

# External libraries

# Internal libraries

# Created: 2/21/24
# as of 3/6/24 this fucking works

# GeoAlchemy ORM Tutorial
# https://geoalchemy-2.readthedocs.io/en/latest/orm_tutorial.html

import docker
import logging
import enum
import os
import time
import traceback


# External libraries
from geoalchemy2 import Geometry
from sqlalchemy import (
create_engine,
Column,
Integer,
schema,
text,
MetaData,
Table,
ForeignKey,
DateTime,
TIMESTAMP,
UUID,
)

from sqlalchemy.orm import declarative_base, sessionmaker


ENV_VARS = {
"MYDB__HOST": "localhost",
"MYDB__DATABASE": "nyc",
"POSTGRES_USER": "nyc",
"MYDB__PORT": 5432,
"MYDB__DRIVERNAME": "postgresql",
"POSTGRES_PASSWORD": "gis",
"platform": "linux/amd64",
}

LOCALHOST = ENV_VARS["MYDB__HOST"]
DB = ENV_VARS["MYDB__DATABASE"]
DRIVERNAME = ENV_VARS["MYDB__DRIVERNAME"]
USER = ENV_VARS["POSTGRES_USER"]
PW = ENV_VARS["POSTGRES_PASSWORD"]
PORT = ENV_VARS["MYDB__PORT"]
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm.state import InstanceState

# Internal libraries

Base = declarative_base()


class Trajectory(Base):
__tablename__ = "trajectory"
__table_args__ = {"schema": "gps"}
id = Column(Integer, primary_key=True)
create_time = Column(TIMESTAMP, default=datetime.now(timezone.utc))
updated_time = Column(TIMESTAMP, default=datetime.now(timezone.utc))
geom = Column(Geometry("LINESTRINGZM"))
feed_item_id = Column(UUID)

def to_dict(self):
d = {}

for field, value in self.__dict__.items():
if any(
(
isinstance(value, InstanceState),
isinstance(value, list),
)
):
continue
if isinstance(value, (int, float, bool, str, type(None))):
d[field] = value
elif isinstance(value, enum.Enum):
d[field] = value.name
else:
d[field] = str(value)

return d


def setup_pg():
"""Create GIS engine, connect, and create tables."""
url = f"{DRIVERNAME}://{USER}:{PW}@{LOCALHOST}:{PORT}/{DB}"
engine = create_engine(url, echo=True)
# Start sqlalchemy engine & wait for connection
engine = engine_go_vroom()
pg_check(engine=engine)

# # Create Postgis extension & check that it works
# # Create Postgis extension and new schema
with engine.connect() as conn:
conn.execute(text("CREATE EXTENSION IF NOT EXISTS postgis"))
conn.execute(text("SELECT postgis_full_version();"))
conn.execute(schema.CreateSchema("gps"))
conn.commit()

# Create Trajectory table in gps schema
with engine.connect() as conn:
Trajectory.__table__.create(engine)

conn.commit()
Expand All @@ -107,30 +103,25 @@ def pg_check(engine, max_retries=10):
raise exc_


# Still needs fixing
def insert_sample_data(engine):
Session = sessionmaker(bind=engine)
session = Session()

# Generate sample data
sample_data = [
Trajectory(
geom="SRID=4326;LINESTRINGZM(0 0 0 0, 1 1 1 1)",
feed_item_id=uuid.uuid4(),
)
for _ in range(20)
]
def engine_go_vroom():
USER = os.getenv("POSTGRES_USER", "nyc")
PW = os.getenv("POSTGRES_PASSWORD", "gis")
DB = os.getenv("POSTGRES_DB", "nyc")
HOST = os.getenv(
"POSTGRES_HOST", "localhost"
) # this localhost only applies if running script locally
DRIVERNAME = os.getenv("POSTGRES_DRIVERNAME", "postgresql")
PORT = os.getenv("PORT", "5432")
url = f"{DRIVERNAME}://{USER}:{PW}@{HOST}:{PORT}/{DB}"
engine = create_engine(url, echo=True)

session.add_all(sample_data)
session.commit()
return engine


if __name__ == "__main__":
try:
setup_pg()
print("DB set up!")
insert_sample_data()
print("Sample data added!")
except Exception as e:
print(traceback.format_exc())
print(e)
72 changes: 72 additions & 0 deletions app/generate_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# By: K Agajanian
# Created: 4/18/24

# Standard libraries
import random
import uuid

# from datetime import datetime

# External libraries
from sqlalchemy.orm import sessionmaker

# Internal libraries
from db_setup import Trajectory, engine_go_vroom


# Pre-determined areas for random data generation
central_park = {"lon": (-73.973, -73.958), "lat": (40.765, 40.800)}
northeast = {"lon": (-70, -75), "lat": (40, 43)}


def main():
"""Enter sample data into Trajectory table."""
engine = engine_go_vroom()
Session = sessionmaker(bind=engine)
session = Session()

for _ in range(5):
try:
# Create rows from sample data
sample_data = [
Trajectory(
geom=f"SRID=4326;LINESTRINGZM({create_sample_linestring(central_park)})",
feed_item_id=uuid.uuid4(),
)
]

session.add_all(sample_data)
session.commit()

except Exception as e:
print(e)
continue


def create_sample_linestring(area):
linestring = ""

num_of_entries = int(random.uniform(1, 5)) # must have at least 2
time = 0

for n in range(num_of_entries):
lon = round(random.uniform(area["lon"][0], area["lon"][1]), 3) # x coordinate
lat = round(random.uniform(area["lat"][0], area["lat"][1]), 3) # y coordinate
alt = round(random.uniform(-1, 1), 0) # altitude
time = time + int(random.uniform(0, 5)) # time
linestring += f"{lon} {lat} {alt} {time},"

# Linestring hates being alone and I don't wanna deal with POINTs
if num_of_entries <= 1:
linestring += linestring

linestring = linestring[:-1]
print(f"HERE'S MY LINESTRING BETCH: {linestring}")
return linestring


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions app/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
geoalchemy2~=0.14.4
psycopg2-binary~=2.9.9
sanic~=23.12.1
sqlalchemy~=2.0.24
16 changes: 9 additions & 7 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
FROM python
FROM python:3.11-slim-bullseye

COPY ./ .
# Set the working directory in the container
WORKDIR /app

ENV POSTGRES_USER nyc
ENV POSTGRES_PASSWORD gis
ENV POSTGRES_DB nyc
# Copy the current directory contents into the container at /app
COPY ../app .

RUN pip install -r requirements.txt

CMD [python db_setup.py]
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Run db_setup.py when the container launches
CMD ["python", "db_setup.py"]
34 changes: 20 additions & 14 deletions docker/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@

version: "3"
x-common-variables: &common-variables
POSTGRES_USER: nyc
POSTGRES_PASSWORD: gis #${POSTGRES_PASSWORD}
POSTGRES_DB: nyc
POSTGRES_HOST: db
POSTGRES_DRIVERNAME: postgresql
PORT: 5432


services:
db:
container_name: geos
container_name: db
image: postgis/postgis:14-3.4
restart: always
environment:
POSTGRES_USER: nyc # ${POSTGRES_USER}
POSTGRES_PASSWORD: gis #${POSTGRES_PASSWORD}
POSTGRES_DB: nyc # ${POSTGRES_DB}
environment: *common-variables
ports:
- "5432:5432"
pgadmin: # Not necessary since using SRID 4326, but keeping it anyway
container_name: pgadmin4_container
image: dpage/pgadmin4
restart: always
environment:
PGADMIN_DEFAULT_EMAIL: [email protected]
PGADMIN_DEFAULT_PASSWORD: root
ports:
- "5050:80"

db-setup: # create Trajectory table
container_name: db-setup
environment: *common-variables
build:
context: ..
dockerfile: docker/Dockerfile

Loading

0 comments on commit 5ea1d5a

Please sign in to comment.