Compare commits
10 Commits
e6e8cf0e42
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de53b760c4 | ||
|
|
6cd8997516 | ||
|
|
dac7c2dbcf | ||
|
|
e6c37f38f5 | ||
|
|
4f3919d02d | ||
|
|
9c1d0b9b32 | ||
|
|
fdaf72f103 | ||
|
|
686e4ff477 | ||
|
|
c7b3f95439 | ||
|
|
ad087b9547 |
@@ -19,6 +19,7 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
|
|
||||||
# Copy application files
|
# Copy application files
|
||||||
COPY app.py .
|
COPY app.py .
|
||||||
|
COPY lib.py .
|
||||||
COPY ingress.py .
|
COPY ingress.py .
|
||||||
COPY models.py .
|
COPY models.py .
|
||||||
|
|
||||||
|
|||||||
75
README.md
75
README.md
@@ -401,6 +401,81 @@ curl "http://localhost:7000/plexts?min_timestamp=2026-01-12T10:00:00Z&max_timest
|
|||||||
curl "http://localhost:7000/plexts?event_types=PORTAL_CAPTURED&player_name=Albicocca&min_timestamp=2026-01-12T10:00:00Z"
|
curl "http://localhost:7000/plexts?event_types=PORTAL_CAPTURED&player_name=Albicocca&min_timestamp=2026-01-12T10:00:00Z"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### GET /plexts/from-db
|
||||||
|
|
||||||
|
Get plexts from MongoDB with optional filters. Returns plexts sorted by timestamp (most recent first).
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
| --------------- | ------- | -------- | --------------------------------------------------- |
|
||||||
|
| `player_name` | String | None | Player name to filter by |
|
||||||
|
| `timestamp_from` | Integer | None | Minimum timestamp in milliseconds (optional) |
|
||||||
|
| `timestamp_to` | Integer | None | Maximum timestamp in milliseconds (optional) |
|
||||||
|
| `limit` | Integer | 100 | Maximum number of results to return |
|
||||||
|
| `center` | String | None | Geographic center point in format "latitude,longitude" (optional, requires radius) |
|
||||||
|
| `radius` | Integer | None | Search radius in meters (optional, requires center) |
|
||||||
|
|
||||||
|
**Geospatial Query:**
|
||||||
|
|
||||||
|
When both `center` and `radius` parameters are provided, the endpoint returns plexts within the specified radius from the center point. The response includes a `distance` field (in meters) for each result.
|
||||||
|
|
||||||
|
**Response with geospatial query:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 5,
|
||||||
|
"plexts": [
|
||||||
|
{
|
||||||
|
"id": "abc123",
|
||||||
|
"timestamp": 1736659207000,
|
||||||
|
"timestamp_formatted": "2026-01-12 11:00:07",
|
||||||
|
"text": "Albicocca captured L' Arboreto",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"plext_type": "PLAYER_GENERATED",
|
||||||
|
"categories": 0,
|
||||||
|
"event_type": "PORTAL_CAPTURED",
|
||||||
|
"player_name": "Albicocca",
|
||||||
|
"portal_name": "L' Arboreto",
|
||||||
|
"coordinates": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [12.365208, 45.573661]
|
||||||
|
},
|
||||||
|
"distance": 125.5,
|
||||||
|
"markup": [...]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Both `center` and `radius` parameters must be provided together. If neither is provided, the endpoint returns all plexts matching other filters without geospatial filtering.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get all plexts from MongoDB
|
||||||
|
curl "http://localhost:7000/plexts/from-db"
|
||||||
|
|
||||||
|
# Filter by player name
|
||||||
|
curl "http://localhost:7000/plexts/from-db?player_name=Albicocca"
|
||||||
|
|
||||||
|
# Filter by time range
|
||||||
|
curl "http://localhost:7000/plexts/from-db?timestamp_from=17366592000000×tamp_to=1736745600000"
|
||||||
|
|
||||||
|
# Geospatial query: Get plexts within 1km of a specific location
|
||||||
|
curl "http://localhost:7000/plexts/from-db?center=45.573661,12.365208&radius=1000"
|
||||||
|
|
||||||
|
# Geospatial query with player filter
|
||||||
|
curl "http://localhost:7000/plexts/from-db?center=45.573661,12.365208&radius=1000&player_name=Albicocca"
|
||||||
|
|
||||||
|
# Geospatial query with time filter
|
||||||
|
curl "http://localhost:7000/plexts/from-db?center=45.573661,12.365208&radius=1000×tamp_from=17366592000000"
|
||||||
|
|
||||||
|
# Combine all filters
|
||||||
|
curl "http://localhost:7000/plexts/from-db?center=45.573661,12.365208&radius=1000&player_name=Albicocca×tamp_from=17366592000000&limit=10"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authentication:** Basic Auth required
|
||||||
|
|
||||||
### Scheduler
|
### Scheduler
|
||||||
|
|
||||||
The scheduler automatically collects Ingress events every minute and stores them in MongoDB.
|
The scheduler automatically collects Ingress events every minute and stores them in MongoDB.
|
||||||
|
|||||||
291
app.py
291
app.py
@@ -1,174 +1,52 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
from flask import Flask, request, jsonify
|
||||||
from datetime import datetime
|
from flask_cors import CORS
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
from functools import wraps
|
|
||||||
from flask import Flask, request, jsonify, Response
|
|
||||||
from ingress import IngressAPI
|
from ingress import IngressAPI
|
||||||
from models import EventType, Plext
|
from models import EventType
|
||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
from pymongo.errors import PyMongoError
|
from pymongo.errors import PyMongoError
|
||||||
|
from dotenv import load_dotenv
|
||||||
# Timezone configuration
|
from lib import (
|
||||||
TIMEZONE = ZoneInfo("Europe/Rome")
|
check_basic_auth,
|
||||||
|
basic_auth_required,
|
||||||
# Configure logging
|
parse_timestamp,
|
||||||
logging.basicConfig(
|
plext_to_dict,
|
||||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
logger,
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
|
||||||
def check_basic_auth(username: str, password: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if the provided username and password match the configured credentials.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: Username from the request
|
|
||||||
password: Password from the request
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if credentials match, False otherwise
|
|
||||||
"""
|
|
||||||
expected_username = os.getenv("BASIC_AUTH_USER")
|
|
||||||
expected_password = os.getenv("BASIC_AUTH_PASSWORD")
|
|
||||||
|
|
||||||
if not expected_username or not expected_password:
|
|
||||||
logger.warning("BASIC_AUTH_USER or BASIC_AUTH_PASSWORD not configured")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return username == expected_username and password == expected_password
|
|
||||||
|
|
||||||
|
|
||||||
def basic_auth_required(f):
|
|
||||||
"""
|
|
||||||
Decorator to require basic authentication for an endpoint.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
401 Unauthorized if authentication fails or is missing
|
|
||||||
"""
|
|
||||||
@wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
auth = request.authorization
|
|
||||||
|
|
||||||
if not auth or not check_basic_auth(auth.username, auth.password):
|
|
||||||
return Response(
|
|
||||||
"Could not verify your access level for that URL.\n"
|
|
||||||
"You have to login with proper credentials",
|
|
||||||
401,
|
|
||||||
{"WWW-Authenticate": 'Basic realm="Login Required"'}
|
|
||||||
)
|
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
|
|
||||||
def parse_timestamp(value: str) -> int:
|
|
||||||
"""
|
|
||||||
Parse timestamp from either milliseconds (int) or ISO 8601 string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value: Either integer milliseconds or ISO 8601 string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Timestamp in milliseconds since epoch
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If format is invalid
|
|
||||||
"""
|
|
||||||
# Try parsing as integer (milliseconds)
|
|
||||||
try:
|
|
||||||
return int(value)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Try parsing as ISO 8601 datetime
|
|
||||||
try:
|
|
||||||
# Handle 'Z' suffix (UTC)
|
|
||||||
iso_value = value.replace("Z", "+00:00")
|
|
||||||
|
|
||||||
# Handle timezone offset without colon (e.g., +0100 -> +01:00)
|
|
||||||
# Match pattern like +0100 or -0100 at the end of the string
|
|
||||||
import re
|
|
||||||
|
|
||||||
match = re.search(r"([+-])(\d{2})(\d{2})$", iso_value)
|
|
||||||
if match:
|
|
||||||
sign, hours, minutes = match.groups()
|
|
||||||
iso_value = re.sub(
|
|
||||||
r"([+-])(\d{2})(\d{2})$", f"{sign}{hours}:{minutes}", iso_value
|
|
||||||
)
|
|
||||||
|
|
||||||
dt = datetime.fromisoformat(iso_value)
|
|
||||||
return int(dt.timestamp() * 1000)
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid timestamp format: {value}. "
|
|
||||||
"Use milliseconds (e.g., 1736659200000) or "
|
|
||||||
"ISO 8601 format (e.g., '2026-01-12T10:00:00Z' or '2026-01-12T10:00:00+01:00')"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def plext_to_dict(plext: Plext) -> dict:
|
|
||||||
"""
|
|
||||||
Convert a Plext object to a dictionary for JSON serialization.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
plext: Plext object to convert
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary representation of the plext
|
|
||||||
"""
|
|
||||||
coords = plext.get_event_coordinates()
|
|
||||||
# Convert timestamp from milliseconds to datetime in Europe/Rome timezone
|
|
||||||
dt = datetime.fromtimestamp(plext.timestamp / 1000.0, tz=TIMEZONE)
|
|
||||||
return {
|
|
||||||
"id": plext.id,
|
|
||||||
"timestamp": plext.timestamp,
|
|
||||||
"timestamp_formatted": dt.strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
"text": plext.text,
|
|
||||||
"team": plext.team,
|
|
||||||
"plext_type": plext.plext_type,
|
|
||||||
"categories": plext.categories,
|
|
||||||
"event_type": plext.get_event_type().name,
|
|
||||||
"player_name": plext.get_player_name(),
|
|
||||||
"portal_name": plext.get_portal_name(),
|
|
||||||
"coordinates": {"lat": coords[0], "lng": coords[1]} if coords else None,
|
|
||||||
"markup": [
|
|
||||||
{
|
|
||||||
"type": m.type,
|
|
||||||
"plain": m.plain,
|
|
||||||
"team": m.team,
|
|
||||||
"name": m.name,
|
|
||||||
"address": m.address,
|
|
||||||
"latE6": m.latE6,
|
|
||||||
"lngE6": m.lngE6,
|
|
||||||
}
|
|
||||||
for m in plext.markup
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.route("/plexts/from-db", methods=["GET"])
|
@app.route("/plexts/from-db", methods=["GET"])
|
||||||
@basic_auth_required
|
@basic_auth_required
|
||||||
def get_plexts_from_db():
|
def get_plexts_from_db():
|
||||||
"""
|
"""
|
||||||
Get plexts from MongoDB with optional filters.
|
Get plexts from MongoDB with optional filters.
|
||||||
|
Returns plexts sorted by timestamp (most recent first).
|
||||||
|
|
||||||
Query Parameters:
|
Query Parameters:
|
||||||
player_name: Filter by player name (optional)
|
player_name: Filter by player name (optional)
|
||||||
timestamp_from: Minimum timestamp in milliseconds (optional)
|
timestamp_from: Minimum timestamp in milliseconds (optional)
|
||||||
timestamp_to: Maximum timestamp in milliseconds (optional)
|
timestamp_to: Maximum timestamp in milliseconds (optional)
|
||||||
|
limit: Maximum number of results to return (optional, default: 100)
|
||||||
|
center: Geographic center point in format "latitude,longitude" (optional, requires radius)
|
||||||
|
radius: Search radius in meters (optional, requires center)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON response with list of plexts (without _id field)
|
JSON response with list of plexts (without _id field), limited by the limit parameter.
|
||||||
|
When using geospatial query (center and radius), includes a 'distance' field in meters.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Parse query parameters
|
# Parse query parameters
|
||||||
player_name = request.args.get("player_name")
|
player_name = request.args.get("player_name")
|
||||||
timestamp_from = request.args.get("timestamp_from")
|
timestamp_from = request.args.get("timestamp_from")
|
||||||
timestamp_to = request.args.get("timestamp_to")
|
timestamp_to = request.args.get("timestamp_to")
|
||||||
|
limit_param = request.args.get("limit")
|
||||||
|
center = request.args.get("center")
|
||||||
|
radius = request.args.get("radius")
|
||||||
|
|
||||||
# Validate and convert timestamp parameters to integers if provided
|
# Validate and convert timestamp parameters to integers if provided
|
||||||
if timestamp_from is not None:
|
if timestamp_from is not None:
|
||||||
@@ -183,6 +61,55 @@ def get_plexts_from_db():
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return jsonify({"error": "timestamp_to must be an integer"}), 400
|
return jsonify({"error": "timestamp_to must be an integer"}), 400
|
||||||
|
|
||||||
|
# Validate and convert limit parameter to integer if provided, otherwise default to 100
|
||||||
|
if limit_param is not None:
|
||||||
|
try:
|
||||||
|
limit = int(limit_param)
|
||||||
|
if limit <= 0:
|
||||||
|
return jsonify({"error": "limit must be a positive integer"}), 400
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({"error": "limit must be an integer"}), 400
|
||||||
|
else:
|
||||||
|
limit = 100
|
||||||
|
|
||||||
|
# Validate geospatial parameters
|
||||||
|
# Both center and radius must be provided together, or neither
|
||||||
|
if (center is not None) != (radius is not None):
|
||||||
|
return jsonify({
|
||||||
|
"error": "Both 'center' and 'radius' parameters must be provided together"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Parse and validate center parameter if provided
|
||||||
|
center_coords = None
|
||||||
|
if center is not None:
|
||||||
|
try:
|
||||||
|
# Parse center in format "latitude,longitude"
|
||||||
|
lat_str, lng_str = center.split(',')
|
||||||
|
lat = float(lat_str)
|
||||||
|
lng = float(lng_str)
|
||||||
|
|
||||||
|
# Validate latitude and longitude ranges
|
||||||
|
if not (-90 <= lat <= 90):
|
||||||
|
return jsonify({"error": "Latitude must be between -90 and 90"}), 400
|
||||||
|
if not (-180 <= lng <= 180):
|
||||||
|
return jsonify({"error": "Longitude must be between -180 and 180"}), 400
|
||||||
|
|
||||||
|
center_coords = (lat, lng)
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({
|
||||||
|
"error": "Invalid center format. Use format: 'latitude,longitude' (e.g., '45.573661,12.365208')"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Parse and validate radius parameter if provided
|
||||||
|
radius_meters = None
|
||||||
|
if radius is not None:
|
||||||
|
try:
|
||||||
|
radius_meters = int(radius)
|
||||||
|
if radius_meters <= 0:
|
||||||
|
return jsonify({"error": "Radius must be a positive integer"}), 400
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({"error": "Radius must be an integer"}), 400
|
||||||
|
|
||||||
# Build MongoDB filter query
|
# Build MongoDB filter query
|
||||||
filter_query = {}
|
filter_query = {}
|
||||||
|
|
||||||
@@ -207,22 +134,61 @@ def get_plexts_from_db():
|
|||||||
collection = db[collection_name]
|
collection = db[collection_name]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Projection to exclude _id field
|
# Ensure geospatial index exists
|
||||||
projection = {"_id": 0}
|
try:
|
||||||
|
collection.create_index([("coordinates", "2dsphere")], background=True)
|
||||||
|
except PyMongoError as e:
|
||||||
|
logger.warning(f"Could not create geospatial index: {e}")
|
||||||
|
|
||||||
# Execute query with sorting (timestamp DESC - most recent first)
|
# Execute query based on whether geospatial parameters are provided
|
||||||
cursor = collection.find(
|
if center_coords and radius_meters:
|
||||||
filter=filter_query,
|
# Use $geoNear aggregation for geospatial query
|
||||||
projection=projection
|
lat, lng = center_coords
|
||||||
).sort("timestamp", -1)
|
pipeline = [
|
||||||
|
{
|
||||||
|
"$geoNear": {
|
||||||
|
"near": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [lng, lat] # GeoJSON uses [longitude, latitude]
|
||||||
|
},
|
||||||
|
"distanceField": "distance", # Add distance to results
|
||||||
|
"maxDistance": radius_meters, # in meters
|
||||||
|
"spherical": True # Use spherical geometry
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$match": filter_query # Apply other filters
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$sort": {"timestamp": -1} # Sort by timestamp (most recent first)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$limit": limit
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$project": {
|
||||||
|
"_id": 0 # Exclude _id field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
# Convert cursor to list
|
# Execute aggregation pipeline
|
||||||
plexts = list(cursor)
|
plexts = list(collection.aggregate(pipeline))
|
||||||
|
else:
|
||||||
|
# Use standard find() query for non-geospatial queries
|
||||||
|
projection = {"_id": 0}
|
||||||
|
|
||||||
return jsonify({
|
# Execute query with sorting (timestamp DESC - most recent first) and dynamic limit
|
||||||
"count": len(plexts),
|
cursor = (
|
||||||
"plexts": plexts
|
collection.find(filter=filter_query, projection=projection)
|
||||||
})
|
.sort("timestamp", -1)
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert cursor to list
|
||||||
|
plexts = list(cursor)
|
||||||
|
|
||||||
|
return jsonify({"count": len(plexts), "plexts": plexts}), 200
|
||||||
|
|
||||||
except PyMongoError as e:
|
except PyMongoError as e:
|
||||||
logger.error(f"MongoDB error: {e}")
|
logger.error(f"MongoDB error: {e}")
|
||||||
@@ -234,6 +200,7 @@ def get_plexts_from_db():
|
|||||||
logger.exception("Unexpected error in get_plexts_from_db")
|
logger.exception("Unexpected error in get_plexts_from_db")
|
||||||
return jsonify({"error": "An error occurred"}), 500
|
return jsonify({"error": "An error occurred"}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route("/plexts/from-api", methods=["GET"])
|
@app.route("/plexts/from-api", methods=["GET"])
|
||||||
@basic_auth_required
|
@basic_auth_required
|
||||||
def get_plexts_from_api():
|
def get_plexts_from_api():
|
||||||
@@ -348,13 +315,21 @@ def index():
|
|||||||
},
|
},
|
||||||
"/plexts/from-db": {
|
"/plexts/from-db": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"description": "Get plexts from MongoDB with optional filters",
|
"description": "Get plexts from MongoDB with optional filters. Returns plexts sorted by timestamp (most recent first).",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"player_name": "Filter by player name (optional)",
|
"player_name": "Filter by player name (optional)",
|
||||||
"timestamp_from": "Minimum timestamp in milliseconds (optional)",
|
"timestamp_from": "Minimum timestamp in milliseconds (optional)",
|
||||||
"timestamp_to": "Maximum timestamp in milliseconds (optional)",
|
"timestamp_to": "Maximum timestamp in milliseconds (optional)",
|
||||||
|
"limit": "Maximum number of results to return (optional, default: 100)",
|
||||||
|
"center": "Geographic center point in format 'latitude,longitude' (optional, requires radius)",
|
||||||
|
"radius": "Search radius in meters (optional, requires center)",
|
||||||
},
|
},
|
||||||
"authentication": "Basic Auth required",
|
"authentication": "Basic Auth required",
|
||||||
|
"geospatial_query": {
|
||||||
|
"description": "When both center and radius are provided, returns plexts within the specified radius from the center point. If neither is provided, returns all plexts matching other filters.",
|
||||||
|
"example": "center=45.573661,12.365208&radius=1000",
|
||||||
|
"response_field": "distance (meters) - included in results when using geospatial query",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Script to convert geographic data in MongoDB from E6 format to GeoJSON Point format.
|
|
||||||
This script updates all documents in the ingress collection to use proper MongoDB GeoJSON format.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.errors import PyMongoError
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def convert_e6_to_degrees(e6_value):
|
|
||||||
"""
|
|
||||||
Convert E6 format to degrees.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
e6_value: Integer value in E6 format (multiply by 1e-6 to get degrees)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Float value in degrees
|
|
||||||
"""
|
|
||||||
return e6_value / 1_000_000 if e6_value else 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def create_geojson_point(lat_e6, lng_e6):
|
|
||||||
"""
|
|
||||||
Create a GeoJSON Point from E6 coordinates.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
lat_e6: Latitude in E6 format
|
|
||||||
lng_e6: Longitude in E6 format
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
GeoJSON Point object or None if coordinates are invalid
|
|
||||||
"""
|
|
||||||
if lat_e6 == 0 and lng_e6 == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
lat = convert_e6_to_degrees(lat_e6)
|
|
||||||
lng = convert_e6_to_degrees(lng_e6)
|
|
||||||
|
|
||||||
# GeoJSON Point format: [longitude, latitude]
|
|
||||||
return {
|
|
||||||
"type": "Point",
|
|
||||||
"coordinates": [lng, lat]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def convert_document(doc):
|
|
||||||
"""
|
|
||||||
Convert a single document to use GeoJSON format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
doc: MongoDB document
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Updated document with GeoJSON coordinates
|
|
||||||
"""
|
|
||||||
updated = False
|
|
||||||
updates = {}
|
|
||||||
|
|
||||||
# Convert coordinates field
|
|
||||||
if "coordinates" in doc and isinstance(doc["coordinates"], dict):
|
|
||||||
coords = doc["coordinates"]
|
|
||||||
if "lat" in coords and "lng" in coords:
|
|
||||||
geojson = create_geojson_point(coords["lat"], coords["lng"])
|
|
||||||
if geojson:
|
|
||||||
updates["coordinates"] = geojson
|
|
||||||
updated = True
|
|
||||||
|
|
||||||
# Convert markup array
|
|
||||||
if "markup" in doc and isinstance(doc["markup"], list):
|
|
||||||
new_markup = []
|
|
||||||
for item in doc["markup"]:
|
|
||||||
new_item = item.copy()
|
|
||||||
if "latE6" in item and "lngE6" in item:
|
|
||||||
geojson = create_geojson_point(item["latE6"], item["lngE6"])
|
|
||||||
if geojson:
|
|
||||||
new_item["location"] = geojson
|
|
||||||
updated = True
|
|
||||||
new_markup.append(new_item)
|
|
||||||
if updated:
|
|
||||||
updates["markup"] = new_markup
|
|
||||||
|
|
||||||
return updates if updated else None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""
|
|
||||||
Main function to convert all documents in the collection.
|
|
||||||
"""
|
|
||||||
# Connect to MongoDB
|
|
||||||
mongo_uri = "mongodb://root:root@localhost:27017/?authSource=admin"
|
|
||||||
db_name = "ingress"
|
|
||||||
collection_name = "ingress"
|
|
||||||
|
|
||||||
print(f"Connecting to MongoDB at {mongo_uri}...")
|
|
||||||
print(f"Database: {db_name}, Collection: {collection_name}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
client = MongoClient(mongo_uri)
|
|
||||||
db = client[db_name]
|
|
||||||
collection = db[collection_name]
|
|
||||||
|
|
||||||
# Count total documents
|
|
||||||
total_docs = collection.count_documents({})
|
|
||||||
print(f"Total documents in collection: {total_docs}")
|
|
||||||
|
|
||||||
if total_docs == 0:
|
|
||||||
print("No documents found. Exiting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Process documents in batches
|
|
||||||
batch_size = 100
|
|
||||||
processed = 0
|
|
||||||
updated = 0
|
|
||||||
skipped = 0
|
|
||||||
|
|
||||||
print(f"\nProcessing documents in batches of {batch_size}...")
|
|
||||||
|
|
||||||
cursor = collection.find({})
|
|
||||||
|
|
||||||
for doc in cursor:
|
|
||||||
processed += 1
|
|
||||||
|
|
||||||
# Convert document
|
|
||||||
updates = convert_document(doc)
|
|
||||||
|
|
||||||
if updates:
|
|
||||||
# Update the document
|
|
||||||
collection.update_one(
|
|
||||||
{"_id": doc["_id"]},
|
|
||||||
{"$set": updates}
|
|
||||||
)
|
|
||||||
updated += 1
|
|
||||||
else:
|
|
||||||
skipped += 1
|
|
||||||
|
|
||||||
# Progress indicator
|
|
||||||
if processed % batch_size == 0:
|
|
||||||
print(f"Processed: {processed}/{total_docs} | Updated: {updated} | Skipped: {skipped}")
|
|
||||||
|
|
||||||
print(f"\nConversion complete!")
|
|
||||||
print(f"Total processed: {processed}")
|
|
||||||
print(f"Total updated: {updated}")
|
|
||||||
print(f"Total skipped: {skipped}")
|
|
||||||
|
|
||||||
except PyMongoError as e:
|
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
finally:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
286
example_api_response.md
Normal file
286
example_api_response.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# Example API Response
|
||||||
|
|
||||||
|
**URL**: http://root:root@127.0.0.1:7001/plexts/from-db?limit=5
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 5,
|
||||||
|
"plexts": [
|
||||||
|
{
|
||||||
|
"categories": 1,
|
||||||
|
"coordinates": {
|
||||||
|
"coordinates": [
|
||||||
|
12.365208,
|
||||||
|
45.573661
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"event_type": "RESONATOR_DEPLOYED",
|
||||||
|
"id": "e3696bc7d78d41168a58a7133de10df4.d",
|
||||||
|
"markup": [
|
||||||
|
{
|
||||||
|
"address": "",
|
||||||
|
"latE6": 0,
|
||||||
|
"lngE6": 0,
|
||||||
|
"name": "",
|
||||||
|
"plain": "TerminateThis",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"type": "PLAYER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "",
|
||||||
|
"latE6": 0,
|
||||||
|
"lngE6": 0,
|
||||||
|
"name": "",
|
||||||
|
"plain": " deployed a Resonator on ",
|
||||||
|
"team": "",
|
||||||
|
"type": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Viale della Resistenza, 20, 30020 Quarto D'altino VE, Italy",
|
||||||
|
"latE6": 45573661,
|
||||||
|
"lngE6": 12365208,
|
||||||
|
"location": {
|
||||||
|
"coordinates": [
|
||||||
|
12.365208,
|
||||||
|
45.573661
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"name": "Murales Del Plaza",
|
||||||
|
"plain": "Murales Del Plaza (Viale della Resistenza, 20, 30020 Quarto D'altino VE, Italy)",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"type": "PORTAL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"player_name": "TerminateThis",
|
||||||
|
"plext_type": "SYSTEM_BROADCAST",
|
||||||
|
"portal_name": "Murales Del Plaza",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"text": "TerminateThis deployed a Resonator on Murales Del Plaza (Viale della Resistenza, 20, 30020 Quarto D'altino VE, Italy)",
|
||||||
|
"timestamp": 1768548977306,
|
||||||
|
"timestamp_formatted": "2026-01-16 08:36:17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"categories": 1,
|
||||||
|
"coordinates": {
|
||||||
|
"coordinates": [
|
||||||
|
12.365208,
|
||||||
|
45.573661
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"event_type": "PORTAL_CAPTURED",
|
||||||
|
"id": "65f356117c654a06aea7f78b37c8291c.d",
|
||||||
|
"markup": [
|
||||||
|
{
|
||||||
|
"address": "",
|
||||||
|
"latE6": 0,
|
||||||
|
"lngE6": 0,
|
||||||
|
"name": "",
|
||||||
|
"plain": "TerminateThis",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"type": "PLAYER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "",
|
||||||
|
"latE6": 0,
|
||||||
|
"lngE6": 0,
|
||||||
|
"name": "",
|
||||||
|
"plain": " captured ",
|
||||||
|
"team": "",
|
||||||
|
"type": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Viale della Resistenza, 20, 30020 Quarto D'altino VE, Italy",
|
||||||
|
"latE6": 45573661,
|
||||||
|
"lngE6": 12365208,
|
||||||
|
"location": {
|
||||||
|
"coordinates": [
|
||||||
|
12.365208,
|
||||||
|
45.573661
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"name": "Murales Del Plaza",
|
||||||
|
"plain": "Murales Del Plaza (Viale della Resistenza, 20, 30020 Quarto D'altino VE, Italy)",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"type": "PORTAL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"player_name": "TerminateThis",
|
||||||
|
"plext_type": "SYSTEM_BROADCAST",
|
||||||
|
"portal_name": "Murales Del Plaza",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"text": "TerminateThis captured Murales Del Plaza (Viale della Resistenza, 20, 30020 Quarto D'altino VE, Italy)",
|
||||||
|
"timestamp": 1768548977306,
|
||||||
|
"timestamp_formatted": "2026-01-16 08:36:17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"categories": 1,
|
||||||
|
"coordinates": {
|
||||||
|
"coordinates": [
|
||||||
|
10.298689,
|
||||||
|
45.515071
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"event_type": "PORTAL_CAPTURED",
|
||||||
|
"id": "6aec6787b5a84393a1aa2dec5dabc9d2.d",
|
||||||
|
"markup": [
|
||||||
|
{
|
||||||
|
"address": "",
|
||||||
|
"latE6": 0,
|
||||||
|
"lngE6": 0,
|
||||||
|
"name": "",
|
||||||
|
"plain": "Sca29",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"type": "PLAYER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "",
|
||||||
|
"latE6": 0,
|
||||||
|
"lngE6": 0,
|
||||||
|
"name": "",
|
||||||
|
"plain": " captured ",
|
||||||
|
"team": "",
|
||||||
|
"type": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "SS 11 Continente, 25086 Rezzato BS, Italy",
|
||||||
|
"latE6": 45515071,
|
||||||
|
"lngE6": 10298689,
|
||||||
|
"location": {
|
||||||
|
"coordinates": [
|
||||||
|
10.298689,
|
||||||
|
45.515071
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"name": "Colli dei Longobardi - percorso della Strada del Vino",
|
||||||
|
"plain": "Colli dei Longobardi - percorso della Strada del Vino (SS 11 Continente, 25086 Rezzato BS, Italy)",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"type": "PORTAL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"player_name": "Sca29",
|
||||||
|
"plext_type": "SYSTEM_BROADCAST",
|
||||||
|
"portal_name": "Colli dei Longobardi - percorso della Strada del Vino",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"text": "Sca29 captured Colli dei Longobardi - percorso della Strada del Vino (SS 11 Continente, 25086 Rezzato BS, Italy)",
|
||||||
|
"timestamp": 1768548962208,
|
||||||
|
"timestamp_formatted": "2026-01-16 08:36:02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"categories": 1,
|
||||||
|
"coordinates": {
|
||||||
|
"coordinates": [
|
||||||
|
10.298689,
|
||||||
|
45.515071
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"event_type": "RESONATOR_DEPLOYED",
|
||||||
|
"id": "e7c15d8d04bd4b43a276241c3c63337e.d",
|
||||||
|
"markup": [
|
||||||
|
{
|
||||||
|
"address": "",
|
||||||
|
"latE6": 0,
|
||||||
|
"lngE6": 0,
|
||||||
|
"name": "",
|
||||||
|
"plain": "Sca29",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"type": "PLAYER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "",
|
||||||
|
"latE6": 0,
|
||||||
|
"lngE6": 0,
|
||||||
|
"name": "",
|
||||||
|
"plain": " deployed a Resonator on ",
|
||||||
|
"team": "",
|
||||||
|
"type": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "SS 11 Continente, 25086 Rezzato BS, Italy",
|
||||||
|
"latE6": 45515071,
|
||||||
|
"lngE6": 10298689,
|
||||||
|
"location": {
|
||||||
|
"coordinates": [
|
||||||
|
10.298689,
|
||||||
|
45.515071
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"name": "Colli dei Longobardi - percorso della Strada del Vino",
|
||||||
|
"plain": "Colli dei Longobardi - percorso della Strada del Vino (SS 11 Continente, 25086 Rezzato BS, Italy)",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"type": "PORTAL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"player_name": "Sca29",
|
||||||
|
"plext_type": "SYSTEM_BROADCAST",
|
||||||
|
"portal_name": "Colli dei Longobardi - percorso della Strada del Vino",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"text": "Sca29 deployed a Resonator on Colli dei Longobardi - percorso della Strada del Vino (SS 11 Continente, 25086 Rezzato BS, Italy)",
|
||||||
|
"timestamp": 1768548962208,
|
||||||
|
"timestamp_formatted": "2026-01-16 08:36:02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"categories": 1,
|
||||||
|
"coordinates": {
|
||||||
|
"coordinates": [
|
||||||
|
12.364941,
|
||||||
|
45.574634
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"event_type": "PORTAL_CAPTURED",
|
||||||
|
"id": "a604bddc4ca34d838fd2eb7e965aea23.d",
|
||||||
|
"markup": [
|
||||||
|
{
|
||||||
|
"address": "",
|
||||||
|
"latE6": 0,
|
||||||
|
"lngE6": 0,
|
||||||
|
"name": "",
|
||||||
|
"plain": "TerminateThis",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"type": "PLAYER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "",
|
||||||
|
"latE6": 0,
|
||||||
|
"lngE6": 0,
|
||||||
|
"name": "",
|
||||||
|
"plain": " captured ",
|
||||||
|
"team": "",
|
||||||
|
"type": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Via Stazione, 155, 30020 Quarto D'altino Venice, Italy",
|
||||||
|
"latE6": 45574634,
|
||||||
|
"lngE6": 12364941,
|
||||||
|
"location": {
|
||||||
|
"coordinates": [
|
||||||
|
12.364941,
|
||||||
|
45.574634
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"name": "Quarto D'Altino - Stazione FS",
|
||||||
|
"plain": "Quarto D'Altino - Stazione FS (Via Stazione, 155, 30020 Quarto D'altino Venice, Italy)",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"type": "PORTAL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"player_name": "TerminateThis",
|
||||||
|
"plext_type": "SYSTEM_BROADCAST",
|
||||||
|
"portal_name": "Quarto D'Altino - Stazione FS",
|
||||||
|
"team": "RESISTANCE",
|
||||||
|
"text": "TerminateThis captured Quarto D'Altino - Stazione FS (Via Stazione, 155, 30020 Quarto D'altino Venice, Italy)",
|
||||||
|
"timestamp": 1768548949420,
|
||||||
|
"timestamp_formatted": "2026-01-16 08:35:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
147
lib.py
Normal file
147
lib.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
from functools import wraps
|
||||||
|
from flask import request, Response
|
||||||
|
from models import Plext
|
||||||
|
|
||||||
|
# Timezone configuration
|
||||||
|
TIMEZONE = ZoneInfo("Europe/Rome")
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def check_basic_auth(username: str, password: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the provided username and password match the configured credentials.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: Username from the request
|
||||||
|
password: Password from the request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if credentials match, False otherwise
|
||||||
|
"""
|
||||||
|
expected_username = os.getenv("BASIC_AUTH_USER")
|
||||||
|
expected_password = os.getenv("BASIC_AUTH_PASSWORD")
|
||||||
|
|
||||||
|
if not expected_username or not expected_password:
|
||||||
|
logger.warning("BASIC_AUTH_USER or BASIC_AUTH_PASSWORD not configured")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return username == expected_username and password == expected_password
|
||||||
|
|
||||||
|
|
||||||
|
def basic_auth_required(f):
|
||||||
|
"""
|
||||||
|
Decorator to require basic authentication for an endpoint.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
401 Unauthorized if authentication fails or is missing
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
auth = request.authorization
|
||||||
|
|
||||||
|
if not auth or not check_basic_auth(auth.username, auth.password):
|
||||||
|
return Response(
|
||||||
|
"Could not verify your access level for that URL.\n"
|
||||||
|
"You have to login with proper credentials",
|
||||||
|
401,
|
||||||
|
{"WWW-Authenticate": 'Basic realm="Login Required"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def parse_timestamp(value: str) -> int:
|
||||||
|
"""
|
||||||
|
Parse timestamp from either milliseconds (int) or ISO 8601 string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Either integer milliseconds or ISO 8601 string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Timestamp in milliseconds since epoch
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If format is invalid
|
||||||
|
"""
|
||||||
|
# Try parsing as integer (milliseconds)
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try parsing as ISO 8601 datetime
|
||||||
|
try:
|
||||||
|
# Handle 'Z' suffix (UTC)
|
||||||
|
iso_value = value.replace("Z", "+00:00")
|
||||||
|
|
||||||
|
# Handle timezone offset without colon (e.g., +0100 -> +01:00)
|
||||||
|
# Match pattern like +0100 or -0100 at the end of the string
|
||||||
|
import re
|
||||||
|
|
||||||
|
match = re.search(r"([+-])(\d{2})(\d{2})$", iso_value)
|
||||||
|
if match:
|
||||||
|
sign, hours, minutes = match.groups()
|
||||||
|
iso_value = re.sub(
|
||||||
|
r"([+-])(\d{2})(\d{2})$", f"{sign}{hours}:{minutes}", iso_value
|
||||||
|
)
|
||||||
|
|
||||||
|
dt = datetime.fromisoformat(iso_value)
|
||||||
|
return int(dt.timestamp() * 1000)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid timestamp format: {value}. "
|
||||||
|
"Use milliseconds (e.g., 1736659200000) or "
|
||||||
|
"ISO 8601 format (e.g., '2026-01-12T10:00:00Z' or '2026-01-12T10:00:00+01:00')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def plext_to_dict(plext: Plext) -> dict:
|
||||||
|
"""
|
||||||
|
Convert a Plext object to a dictionary for JSON serialization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plext: Plext object to convert
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary representation of the plext
|
||||||
|
"""
|
||||||
|
coords = plext.get_event_coordinates()
|
||||||
|
# Convert timestamp from milliseconds to datetime in Europe/Rome timezone
|
||||||
|
dt = datetime.fromtimestamp(plext.timestamp / 1000.0, tz=TIMEZONE)
|
||||||
|
return {
|
||||||
|
"id": plext.id,
|
||||||
|
"timestamp": plext.timestamp,
|
||||||
|
"timestamp_formatted": dt.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"text": plext.text,
|
||||||
|
"team": plext.team,
|
||||||
|
"plext_type": plext.plext_type,
|
||||||
|
"categories": plext.categories,
|
||||||
|
"event_type": plext.get_event_type().name,
|
||||||
|
"player_name": plext.get_player_name(),
|
||||||
|
"portal_name": plext.get_portal_name(),
|
||||||
|
"coordinates": {"lat": coords[0], "lng": coords[1]} if coords else None,
|
||||||
|
"markup": [
|
||||||
|
{
|
||||||
|
"type": m.type,
|
||||||
|
"plain": m.plain,
|
||||||
|
"team": m.team,
|
||||||
|
"name": m.name,
|
||||||
|
"address": m.address,
|
||||||
|
"latE6": m.latE6,
|
||||||
|
"lngE6": m.lngE6,
|
||||||
|
}
|
||||||
|
for m in plext.markup
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -12,4 +12,5 @@ dependencies = [
|
|||||||
"pymongo>=4.10.0",
|
"pymongo>=4.10.0",
|
||||||
"apscheduler>=3.10.0",
|
"apscheduler>=3.10.0",
|
||||||
"ipython>=9.9.0",
|
"ipython>=9.9.0",
|
||||||
|
"flask-cors>=6.0.2",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
apscheduler==3.11.2
|
apscheduler==3.11.2
|
||||||
|
asttokens==3.0.1
|
||||||
blinker==1.9.0
|
blinker==1.9.0
|
||||||
certifi==2026.1.4
|
certifi==2026.1.4
|
||||||
charset-normalizer==3.4.4
|
charset-normalizer==3.4.4
|
||||||
click==8.3.1
|
click==8.3.1
|
||||||
|
decorator==5.2.1
|
||||||
dnspython==2.8.0
|
dnspython==2.8.0
|
||||||
|
executing==2.2.1
|
||||||
flask==3.1.2
|
flask==3.1.2
|
||||||
|
flask-cors==6.0.2
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
idna==3.11
|
idna==3.11
|
||||||
|
ipython==9.9.0
|
||||||
|
ipython-pygments-lexers==1.1.1
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
|
jedi==0.19.2
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
markupsafe==3.0.3
|
markupsafe==3.0.3
|
||||||
|
matplotlib-inline==0.2.1
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
|
parso==0.8.5
|
||||||
|
pexpect==4.9.0
|
||||||
|
prompt-toolkit==3.0.52
|
||||||
|
ptyprocess==0.7.0
|
||||||
|
pure-eval==0.2.3
|
||||||
|
pygments==2.19.2
|
||||||
pymongo==4.16.0
|
pymongo==4.16.0
|
||||||
python-dotenv==1.2.1
|
python-dotenv==1.2.1
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
|
stack-data==0.6.3
|
||||||
|
traitlets==5.14.3
|
||||||
tzlocal==5.3.1
|
tzlocal==5.3.1
|
||||||
urllib3==2.6.3
|
urllib3==2.6.3
|
||||||
|
wcwidth==0.2.14
|
||||||
werkzeug==3.1.5
|
werkzeug==3.1.5
|
||||||
|
|||||||
15
uv.lock
generated
15
uv.lock
generated
@@ -147,6 +147,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask-cors"
|
||||||
|
version = "6.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "flask" },
|
||||||
|
{ name = "werkzeug" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gunicorn"
|
name = "gunicorn"
|
||||||
version = "23.0.0"
|
version = "23.0.0"
|
||||||
@@ -175,6 +188,7 @@ source = { virtual = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "apscheduler" },
|
{ name = "apscheduler" },
|
||||||
{ name = "flask" },
|
{ name = "flask" },
|
||||||
|
{ name = "flask-cors" },
|
||||||
{ name = "gunicorn" },
|
{ name = "gunicorn" },
|
||||||
{ name = "ipython" },
|
{ name = "ipython" },
|
||||||
{ name = "pymongo" },
|
{ name = "pymongo" },
|
||||||
@@ -186,6 +200,7 @@ dependencies = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "apscheduler", specifier = ">=3.10.0" },
|
{ name = "apscheduler", specifier = ">=3.10.0" },
|
||||||
{ name = "flask", specifier = ">=3.1.2" },
|
{ name = "flask", specifier = ">=3.1.2" },
|
||||||
|
{ name = "flask-cors", specifier = ">=6.0.2" },
|
||||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||||
{ name = "ipython", specifier = ">=9.9.0" },
|
{ name = "ipython", specifier = ">=9.9.0" },
|
||||||
{ name = "pymongo", specifier = ">=4.10.0" },
|
{ name = "pymongo", specifier = ">=4.10.0" },
|
||||||
|
|||||||
Reference in New Issue
Block a user