Skip to content

Commit

Permalink
chore: Merged master
Browse files Browse the repository at this point in the history
  • Loading branch information
frgfm committed Dec 1, 2020
2 parents b3de428 + 129ff9d commit b701b75
Show file tree
Hide file tree
Showing 16 changed files with 347 additions and 90 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,13 @@ jobs:
- uses: actions/checkout@v2
- name: Build & run docker
run: PORT=8002 docker-compose up -d --build
- name: Install dependencies in docker
run: |
PORT=8002 docker-compose exec -T web python -m pip install --upgrade pip
PORT=8002 docker-compose exec -T web pip install -r requirements-dev.txt
- name: Run docker test
run: |
PORT=8002 docker-compose exec -T web coverage --version
PORT=8002 docker-compose exec -T web coverage run -m pytest .
PORT=8002 docker-compose exec -T web coverage xml
docker cp pyro-api_web_1:/usr/src/app/coverage.xml .
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ In order to run the same unit tests as the CI workflows, you can run the dockeri

```bash
PORT=8002 docker-compose up -d --build
PORT=8002 docker-compose exec -T web pip install -r requirements-dev.txt
PORT=8002 docker-compose exec -T web coverage run -m pytest .
```
Please note that you can pick another port number, it only has to be consistent once you have started your containers.
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ RUN set -eux \
&& rm -rf /root/.cache/pip

# copy project
COPY src/ /usr/src/app/
COPY src/ /usr/src/app/
26 changes: 19 additions & 7 deletions client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ Client for the [Pyronear API](https://github.com/pyronear/pyro-api)

## Table of Contents

- [API Client](#pyro-client)
- [API Client](#api-client)
- [Table of Contents](#table-of-contents)
- [Getting started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Usage](#usage)
- [License](#license)
- [Documentation](#documentation)


Expand Down Expand Up @@ -55,14 +56,25 @@ api_client = client.Client(API_URL, CREDENTIALS_LOGIN, CREDENTIALS_PASSWORD)

Use it to query alerts:
```python
# Send alerts:
api_client.send_alert()

# Send medias:
api_client.send_medias()
#AS A DEVICE:
## Create a device
event_id = api_client.create_event(lat=10, lon=10).json()["id"]
## Create a media
media_id = api_client.create_media_from_device().json()["id"]
## Create an alert linked to the media and the event
api_client.send_alert_from_device(lat=10, lon=10, event_id=event_id, media_id=media_id)

## Upload an image on the media
dummy_image = "https://ec.europa.eu/jrc/sites/jrcsh/files/styles/normal-responsive/" \
+ "public/growing-risk-future-wildfires_adobestock_199370851.jpeg"
image_data = requests.get(dummy_image)
api_client.upload_media(media_id=media_id, image_data=image_data.content)

## Update your position:
api_client.update_my_location(lat=1, lon=2, pitch=3)


# Update your position:
api_client.update_location(lat, lon)

```

Expand Down
77 changes: 64 additions & 13 deletions client/pyroclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
from .exceptions import HTTPRequestException
from urllib.parse import urljoin

import io

__all__ = ['Client']

Expand All @@ -21,9 +21,12 @@ class Client:
routes = {"token": "/login/access-token",
"heartbeat": "/device/heartbeat",
"update-my-location": "/device/update-my-location",
"create-event": "/events",
"send-alert": "/alerts",
"send-alert-from-device": "/alerts/from-device",
"create-media": "/media",
"upload-media": "/media/upload",
"create-media-from-device": "/media/from-device",
"upload-media": "/media/{media_id}/upload",
"get-my-devices": "/devices/my-devices",
"get-sites": "/sites",
"get-alerts": "/alerts",
Expand Down Expand Up @@ -57,25 +60,73 @@ def _retrieve_token(self, login, password):
raise HTTPRequestException(response.status_code, response.text)

# Device functions
def hearbeat(self):
def heartbeat(self):
"""Updates the last ping of the device"""
return requests.put(self.routes["heartbeat"], headers=self.headers)

def update_my_location(self):
def update_my_location(self, lat: float = None, lon: float = None,
elevation: float = None, yaw: float = None, pitch: float = None):
"""Updates the location of the device"""
return requests.put(self.routes["update-my-location"], headers=self.headers)

def send_alert(self):
payload = {}

if lat is not None:
payload["lat"] = lat
if lon is not None:
payload["lon"] = lon
if elevation is not None:
payload["elevation"] = elevation
if yaw is not None:
payload["yaw"] = yaw
if pitch is not None:
payload["pitch"] = pitch

if len(payload) == 0:
raise ValueError("At least one location information"
+ "(lat, lon, elevation, yaw, pitch) must be filled")

return requests.put(self.routes["update-my-location"], headers=self.headers, json=payload)

def create_event(self, lat: float, lon: float):
"""Notify an event (e.g wildfire)."""
payload = {"lat": lat,
"lon": lon}
return requests.post(self.routes["create-event"], headers=self.headers, json=payload)

def send_alert(self, lat: float, lon: float, event_id: int, device_id: int, media_id: int = None):
"""Raise an alert to the API"""
return requests.post(self.routes["send-alert"], headers=self.headers)

def create_media(self):
payload = {"lat": lat,
"lon": lon,
"event_id": event_id,
"device_id": device_id
}
if isinstance(media_id, int):
payload["media_id"] = media_id

return requests.post(self.routes["send-alert"], headers=self.headers, json=payload)

def send_alert_from_device(self, lat: float, lon: float, event_id: int, media_id: int = None):
"""Raise an alert to the API from a device (no need to specify device ID)."""
payload = {"lat": lat,
"lon": lon,
"event_id": event_id
}
if isinstance(media_id, int):
payload["media_id"] = media_id

return requests.post(self.routes["send-alert-from-device"], headers=self.headers, json=payload)

def create_media(self, device_id: int):
"""Create a media entry"""
return requests.post(self.routes["create-media"], headers=self.headers)
return requests.post(self.routes["create-media"], headers=self.headers, json={"device_id": device_id})

def create_media_from_device(self):
"""Create a media entry from a device (no need to specify device ID)."""
return requests.post(self.routes["create-media-from-device"], headers=self.headers, json={})

def upload_media(self):
def upload_media(self, media_id: int, image_data: bytes):
"""Upload the media content"""
return requests.post(self.routes["upload-media"], headers=self.headers)
return requests.post(self.routes["upload-media"].format(media_id=media_id), headers=self.headers,
files={'file': io.BytesIO(image_data)})

# User functions
def get_my_devices(self):
Expand Down
8 changes: 0 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,4 @@ python-jose>=3.2.0
passlib[bcrypt]>=1.7.4
python-multipart==0.0.5
aiofiles==0.6.0

# dev
pytest>=5.3.2
pytest-asyncio>=0.14.0
requests>=2.22.0
asyncpg>=0.20.0
coverage>=4.5.4
aiosqlite>=0.16.0
httpx>=0.16.1
28 changes: 23 additions & 5 deletions src/app/api/routes/accesses.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,44 @@ async def update_access_pwd(payload: Cred, entry_id: int = Path(..., gt=0)) -> D
return {"login": entry["login"]}


@router.post("/", response_model=AccessRead, status_code=201)
@router.post("/", response_model=AccessRead, status_code=201, summary="Create a new access")
async def create_access(payload: AccessAuth):
"""
If the provided login does not exist, creates a new access based on the given information
Below, click on "Schema" for more detailed information about arguments
or "Example Value" to get a concrete idea of arguments
"""
return await post_access(payload.login, payload.password, payload.scopes)


@router.get("/{access_id}/", response_model=AccessRead)
@router.get("/{access_id}/", response_model=AccessRead, summary="Get information about a specific access")
async def get_access(access_id: int = Path(..., gt=0)):
"""
Based on a access_id, retrieves information about the specified access
"""
return await crud.get_entry(accesses, access_id)


@router.get("/", response_model=List[AccessRead])
@router.get("/", response_model=List[AccessRead], summary="Get the list of all accesses")
async def fetch_accesses():
"""
Retrieves the list of all accesses and their information
"""
return await crud.fetch_all(accesses)


@router.put("/{access_id}/", response_model=AccessRead)
@router.put("/{access_id}/", response_model=AccessRead, summary="Update information about a specific access")
async def update_access(payload: AccessBase, access_id: int = Path(..., gt=0)):
"""
Based on a access_id, updates information about the specified access
"""
return await crud.update_entry(accesses, payload, access_id)


@router.delete("/{access_id}/", response_model=AccessRead)
@router.delete("/{access_id}/", response_model=AccessRead, summary="Delete a specific access")
async def delete_access(access_id: int = Path(..., gt=0)):
"""
Based on a access_id, deletes the specified access
"""
return await crud.delete_entry(accesses, access_id)
63 changes: 49 additions & 14 deletions src/app/api/routes/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,25 @@
router = APIRouter()


@router.post("/", response_model=AlertOut, status_code=201)
async def check_media_existence(media_id):
existing_media = await crud.fetch_one(media, {"id": media_id})
if existing_media is None:
raise HTTPException(
status_code=404,
detail="Media does not exist"
)


@router.post("/", response_model=AlertOut, status_code=201, summary="Create a new alert")
async def create_alert(payload: AlertIn):
"""
Creates a new alert based on the given information
Below, click on "Schema" for more detailed information about arguments
or "Example Value" to get a concrete idea of arguments
"""
if payload.media_id is not None:
await check_media_existence(payload.media_id)
return await crud.create_entry(alerts, payload)


Expand All @@ -24,33 +41,50 @@ async def create_alert_from_device(payload: AlertBase,
Below, click on "Schema" for more detailed information about arguments
or "Example Value" to get a concrete idea of arguments
"""
if payload.media_id is not None:
await check_media_existence(payload.media_id)
return await crud.create_entry(alerts, AlertIn(**payload.dict(), device_id=device.id))


@router.get("/{alert_id}/", response_model=AlertOut)
@router.get("/{alert_id}/", response_model=AlertOut, summary="Get information about a specific alert")
async def get_alert(alert_id: int = Path(..., gt=0)):
"""
Based on a alert_id, retrieves information about the specified alert
"""
return await crud.get_entry(alerts, alert_id)


@router.get("/", response_model=List[AlertOut])
@router.get("/", response_model=List[AlertOut], summary="Get the list of all alerts")
async def fetch_alerts():
"""
Retrieves the list of all alerts and their information
"""
return await crud.fetch_all(alerts)


@router.put("/{alert_id}/", response_model=AlertOut)
@router.put("/{alert_id}/", response_model=AlertOut, summary="Update information about a specific alert")
async def update_alert(payload: AlertIn, alert_id: int = Path(..., gt=0)):
"""
Based on a alert_id, updates information about the specified alert
"""
return await crud.update_entry(alerts, payload, alert_id)


@router.delete("/{alert_id}/", response_model=AlertOut)
async def delete_alert(alert_id: int = Path(..., gt=0)):
async def delete_alert(alert_id: int = Path(..., gt=0), summary="Delete a specific alert"):
"""
Based on a alert_id, deletes the specified alert
"""
return await crud.delete_entry(alerts, alert_id)


@router.put("/{alert_id}/link-media", response_model=AlertOut)
@router.put("/{alert_id}/link-media", response_model=AlertOut, summary="Link an alert to a media")
async def link_media(payload: AlertMediaId,
alert_id: int = Path(..., gt=0),
current_device: DeviceOut = Security(get_current_device, scopes=["device"])):
"""
Based on a alert_id, and media information as arguments, link the specified alert to a media
"""
# Check that alert is linked to this device
existing_alert = await crud.fetch_one(alerts, {"id": alert_id, "device_id": current_device.id})
if existing_alert is None:
Expand All @@ -59,23 +93,24 @@ async def link_media(payload: AlertMediaId,
detail="Permission denied"
)

existing_media = await crud.fetch_one(media, {"id": payload.media_id})
if existing_media is None:
raise HTTPException(
status_code=404,
detail="Media does not exist"
)
await check_media_existence(payload.media_id)
existing_alert = dict(**existing_alert)
existing_alert["media_id"] = payload.media_id
return await crud.update_entry(alerts, AlertIn(**existing_alert), alert_id)


@router.get("/ongoing", response_model=List[AlertOut])
@router.get("/ongoing", response_model=List[AlertOut], summary="Get the list of ongoing alerts")
async def fetch_ongoing_alerts():
"""
Retrieves the list of ongoing alerts and their information
"""
return await crud.fetch_ongoing_alerts(alerts, {"type": AlertType.start},
excluded_events_filter={"type": AlertType.end})


@router.get("/unacknowledged", response_model=List[AlertOut])
@router.get("/unacknowledged", response_model=List[AlertOut], summary="Get the list of non confirmed alerts")
async def fetch_unacknowledged_alerts():
"""
Retrieves the list of non confirmed alerts and their information
"""
return await crud.fetch_all(alerts, {"is_acknowledged": False})
Loading

0 comments on commit b701b75

Please sign in to comment.