From f8766ba0b8f464137c0007d3f02463a9c1bc6bcc Mon Sep 17 00:00:00 2001 From: Raphael Rivas Date: Wed, 14 Jun 2023 17:17:56 -0300 Subject: [PATCH] Release/2.4.0 (#171) PR: #171 Ref: #169 --- README.md | 528 ++++-------------- .../mobilidade_rio/config_django_q/tasks.py | 11 +- .../mobilidade_rio/pontos/serializers.py | 5 +- mobilidade_rio/mobilidade_rio/pontos/utils.py | 44 +- mobilidade_rio/mobilidade_rio/pontos/views.py | 117 ++-- .../mobilidade_rio/predictor/utils.py | 102 ++-- 6 files changed, 300 insertions(+), 507 deletions(-) diff --git a/README.md b/README.md index e70e455..753244b 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,117 @@ # mobilidade-rio-api -API estática do aplicativo de [pontos.mobilidade.rio](http://pontos.mobilidade.rio) da Prefeitura da cidade do Rio de Janeiro. +API estática do aplicativo [mobilidade.rio](http://mobilidade.rio) da Prefeitura do Rio de Janeiro. + +## Documentação + +Acesse a [wiki](https://github.com/RJ-SMTR/mobilidade-rio-api/wiki) para saber maiores detalhes do projeto, como: +- Endpoints +- Problemas comuns +- Links úteis +- Arquitetura +- Desenvolvimento local +- Exemplos e tutoriais ## Requerimentos -Em parêntesis, o [modo de execução](#modo-de-execução) que utiliza o recurso. +| Ferramenta | Modo de Execução | +|-------------------------|-------------------| +| Python >=3.9 | _Todos_ | +| Docker | docker | +| Postgres | native | + +## Modo de execução + +Modos de execução do Django. + +### Como funciona? -* [Docker](https://www.docker.com/) (local, dev) -* [Kubernetes kubectl](https://kubernetes.io/docs/tasks/tools/) (dev, staging, prod) -* [Kubernetes Lens](https://k8slens.dev/) (dev, staging, prod) -* [Postgres](https://www.postgresql.org/) (nativo) -* Python >=3.9 +Em `mobilidade_rio/mobilidade_rio/settings` você encontra as configurações do Django. + +Para desenvolvimento local você pode criar configurações extras na subpasta `/local_dev`: +```bash +📂 settings/ + 🐍 base.py + 🐍 dev.py + 🐍 stag.py + 🐍 prod.py + 📂 local_dev/ # configs locais + 🐍 native.py + 🐍 docker.py +``` + +Dentro de `local_dev` você pode criar sua própria configuração, dois exemplos recomendados são `native` e `docker`. ## Desenvolvimento local -### Iniciando o ambiente +### Arquivos de desenvolvimento local + +Para configurar e usar algum arquivo para desenvolvimento local, basta criar em qualquer lugar uma pasta chamada `local_dev`. +### Criando o ambiente + +Criando ambiente virtual ```bash +conda create -n mobilidade_rio_api python=3.9 conda activate mobilidade_rio_api pip install -r mobilidade_rio/requirements.txt -r requirements-dev.txt ``` -### Configurando a aplicação +Criando arquivos de desenvolvimento local: +```bash +📂 mobilidade_rio/ # projeto Django + ... + 📂 local_dev/ + 🐋 Docker.py + 🐋 docker-compose.py + ⚙️ native.env + ... + 📂 mobilidade_rio/ # app principal + 📂 settings/ + ... + 📂 local_dev/ + 🐍 native.py # sem Docker ou k8s + 🐍 docker.py +``` -> Para dúvidas sobre usar **Native** ou **Local**, veja [modos de execução do Django](#modos-de-execução-do-django) +> Para exemplos desses arquivos, veja nesta [página da Wiki](https://github.com/RJ-SMTR/mobilidade-rio-api/wiki/Desenvolvimento#Arquivos-de-desenvolvimento-local). + +### Configurando a aplicação Deverá ser executado toda vez que abrir uma nova sessão no terminal. -Native: +native: * Bash ```bash - export DJANGO_SETTINGS_MODULE="mobilidade_rio.settings.native" + export DJANGO_SETTINGS_MODULE="mobilidade_rio.settings.local_dev.native" ``` * Powershell ```powershell - $env:DJANGO_SETTINGS_MODULE="mobilidade_rio.settings.native" + $env:DJANGO_SETTINGS_MODULE="mobilidade_rio.settings.local_dev.native" ``` -Local: +docker: * Bash ```bash - source mobilidade_rio/dev_local/api.env + source mobilidade_rio/local_dev/api.env ``` * Powershell ```powershell - $(Get-Content ./mobilidade_rio/dev_local/api.env | ForEach-Object { $name, $value = $_.split('=');set-content env:\$name $value }); + $(Get-Content ./mobilidade_rio/local_dev/api.env | ForEach-Object { $name, $value = $_.split('=');set-content env:\$name $value }); ``` ### Iniciando a aplicação -Native: +native: ```bash python mobilidade_rio/manage.py makemigrations python mobilidade_rio/manage.py migrate python mobilidade_rio/manage.py runserver 8001 ``` -Local: +docker: ```bash docker-compose -f "mobilidade_rio/dev_local/docker-compose_local.yml" up --build ``` @@ -67,412 +120,71 @@ Dev, Stag e Prod: * O deploy e execução das branches de dev, staging e produção são feitos automaticamente via [Github Actions](https://github.com/features/actions). * Essas branches usam a configuração Django de acordo com seu nome. Exemplo: a branch `dev` usa a configuração dev. + ### Acessando a aplicação URL base para acessar a aplicação: -* Native: `localhost:8001` -* Local: `localhost:8010` -* Dev: `https://api.dev.mobilidade.rio` -* Stag: `https://api.staging.mobilidade.rio` -* Prod: `https://api.mobilidade.rio` - - -Endpoints: - -* `` - API Root com todos os endpoints disponíveis -* `/gtfs` - Endpoints para acessar os dados do GTFS -* `/predictor` - Endpoints para acessar os dados do predictor - -Veja todos os endpoints em - +* native: `localhost:8001` (sugerido) +* docker: `localhost:8010` (sugerido) +* dev: `https://api.dev.mobilidade.rio` +* stag: `https://api.staging.mobilidade.rio` +* prod: `https://api.mobilidade.rio` ### Acessando o banco de dados: -> No Docker ou Kubernetes, são criados os containers `django_hd` (API) e `postgres_hd` (banco). - -`local` e `dev`: - -```bash -docker exec -it django_hd bash -``` - -Para acessar o banco via linha de comando (ainda está vazio!), basta rodar: - -```sh -docker exec -it postgres_hd psql -U postgres -``` - -> Veja mais sobre os comandos do psql [aqui](https://www.postgresql.org/docs/9.1/app-psql.html). - -Para resetar a aplicação do zero, remova os containers e volumes -associados: - -```sh -docker-compose -f ./mobilidade_rio/docker-compose.yml down -v && docker image prune -f -``` - -### Populando o banco - -1. Veja se seu banco está acessível: - - Native: - * Você irá subir para um Postgres instalado na sua máquina. A porta deve ser `5432`. - - Local: - * Você irá subir para um Postgres dentro de um Docker local, a porta deve ser `5433`. - - Dev, Stag, Prod: - * Acesse seu Pod K8s e faça um port foward para uma porta da sua máquina. Vamos supor que seja `5434`. - -2. Verifique em `populate_db.yaml` se as credenciais do seu banco estão corretas. +Recomenda-se o [pgAdmin](https://www.pgadmin.org/) para gerenciar o banco de dados. -3. Salve os arquivos do GTFS na pasta - `scripts/populate_db/fixtures/pontos`. - A estrutura deve seguir neste padrão: - ``` - Pastas tabela no banco +### Populando o banco de dados - - 📂fixtures - - 📂pontos - - 🗒️stops.txt pontos_stops - - 🗒️shapes.txt pontos_shapes - - 🗒️trips.txt pontos_trips - ... - ``` +**1. Carregue os dados no servidor (pgAdmin web)** -4. Execute a subida dos dados: -```sh -python scripts/populate_db/populate_db.py -``` - -O arquivo `populate_db.yaml` contém as configurações para popular o banco -(nomes das tabelas, ordem, parâmetros para o upload). - - > **Os dados subiram?** - > Você pode listar as tabelas [pelo shell do - > Postgres](#acessando-o-banco-local) com o comando `\d`. Elas estarão nomeadas como `pontos_[model]` (ex: `pontos_agency`). - -### Alterando os modelos - - - -Os modelos estão definidos em `mobilidade_rio/models.py`. Para registrar mudanças feitas nesse arquivo (migrações), rode: - -```sh -python mobilidade_rio/manage.py makemigrations -python mobilidade_rio/manage.py migrate -``` - -> Esses comando são executados automaticamente quando o container é criado. +- Vá no menu Tools > Storage Manager +- Crie e entre na pasta chamada `backup` ou similar +- Na janela do Storage Manager clique no botão `...` > `Upload` +- Selecione todos os arquivos desejados + > ⚠️ Não selecione uma pasta inteira, pode causar falha no upload -Pronto! Basta acessar a API e ver os dados (ex: ). +**2. Esvazie todas as tabelas:** +- Em seu database > schema > public, abra o Query Tool. +- Esvazie as tabelas rodando esta query: -## Produção - -> TODO: Revisar e atualizar comandos - -### Acessar o ambiente - -Para acessar o ambiente de Staging localmente, rode: - -```sh -kubectl exec -it -n mobilidade-v2-staging deploy/smtr-stag-mobilidade-api -- /bin/bash -``` - -### Como deletar dados - -Para esvaziar as tabelas rode: - -```sh -scripts/populate_db/populate_db.py --empty_tables --no_insert +```sql +-- Truncate tables +TRUNCATE +pontos_agency, +pontos_calendar, +pontos_calendardates, +pontos_frequencies, +pontos_routes, +pontos_shapes, +pontos_stops, +pontos_stoptimes, +pontos_trips +RESTART IDENTITY CASCADE ``` -- Para esvaziar todo o banco de dados, rode: - ```sh - scripts/populate_db/populate_db.py --empty_db - ``` - -O que NÃO pode alterar ali sem quebrar o Kubernetes: - -* Dockerfile -* setup.sh - -## Endpoints - -### Pontos (`/gtfs`) - -Todos os endpoints estão no endereço `/gtfs`. - -#### stops - -Endereço: `/gtfs/stops` - -Parâmetros: - -* `stop_code` - Filtra por 1 ou mais stop_code - * Uso: `stop_code=1,2,3` - * Exemplo real: - -* `stop_name` - Filtra por 1 ou mais stop_name, não diferencia maiúsculas de minúsculas - * Uso: `stop_name=AB,cd,Ef` - * Exemplo real: - -* `stop_id` - Filtra por 1 ou mais stop_id - * Uso: `stop_id=1,2,3` - * Exemplo real: - -#### stop_times - -Endereço: `/gtfs/stop_times` - -Parâmetros: - -* `trip_id` - Filtra por 1 ou mais trip_id - * Uso: `trip_id=1,2,3` - * Exemplo real: - -* `stop_id` - Filtra por 1 ou mais stop_id - * Uso: `stop_id=1,2,3` - * Funcionamento: - * Se o stop não possuir filhos (`location_type`=0), retorna apenas ele mesmo. - * Exemplo real: - * Se o stop for `parent_station` de alguém (`location_type`=1), retorna apenas seus filhos. - * Exemplo real: - * ⚠️ Por enquanto não é possível pesquisar por `stop_id` e seus filhos ao mesmo tempo. O primeiro item deste parâmetro valerá para os demais. - -* `stop_id__all` - Filtra por 1 ou mais stop_id, onde as trips combinam com todos os stops passados. - > **Por exemplo:** - > Se stop_id__all = `1`, `2`, `3` retorna as ips > `a`, `b`. - > **O resultado será:** - > | stop_id | trip_id | - > | --- | :--| - > |1|a| - > |1|b| - > |2|a| - > |2|b| - > |3|a| - > |3|b| - * Uso: `stop_id__all=1,2,3` - * Exemplo real: - -* É possível combinar todos os parâmetros acima. - * Exemplo: `trip_id=a,b&stop_id=1,2,3&stop_id__all=2,3,4` - * Exemplo real: - -## Apps Django - -### Utils +**3. Carregue os dados para as tabelas** -Contém funções úteis usadas em outros apps. +- Selecionar tabela por tabela +- Clicar no menu `Tools` > `Import/Export data` +- Configurar o Filename e o formato +- Dentro da janela de Import/Export, selecione o menu Columns e confira se a ordem das colunas é exatamente a mesma +- Clique em OK -**query_utils** +**4. Confira se os dados subiram** -Funções para separar a lógica de queries do código e facilitar a manutenção em queries complexas. - -Sempre que possível evite usar queries cruas, use o ORM do Django. Caso contrário, use ou crie uma função em `query_utils`. - -`query_utils.ipynb` é um notebook feito para testar as funções de `query_utils`. Testado na [extensão do VSCode](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). - -## Gerenciando o projeto - -### Modos de execução do Django - -Para testar em sua máquina local: -- **native** - Executa na sua máquina real (barebone), sem usar Docker. -- **local** - Executa dentro de um contêiner Docker em na máquina real. - -Para branches `dev`, `feat/*`, `fix/*` e similares: -- **dev** - Via automação, executa num pod K8s reseravdo para branches dev. - -Para branches `staging`, `release/*` e similares: -- **stag** - Via automação, executa num pod K8s reseravdo para branches de staging ([pré-produção](https://pt.wikipedia.org/wiki/Ambiente_de_implanta%C3%A7%C3%A3o#:~:text=um%20ambiente%20de%20preparacao%20(do%20ingles%20staging)%20ou%20pre-producao%20)). - -Para branch `main`: -- **prod** - Via automação, executa num pod K8s reseravdo para branches dev. - - -> O modo **base** contém as configurações base para todos os modos de execução. - -### Branches do projeto - -* **main** - * _Produção_ - * Branch principal do projeto, onde o código está em produção. - -* **staging** - * _Testar aplicação para produção, mas em servidor de desenvolvimento_ - * Quando o `dev` estiver funcionando corretamente, o código é enviado para o `staging`. - -* **dev** - * _Desenvolvimento_ - * Branch de desenvolvimento, onde o código está em desenvolvimento. - -* ⚠️ **dev-local** - * _Desenvolvimento local_ - * Criado inicialmente para permitir o desenvolvimento local, sem a necessidade de um servidor remoto. - * Talvez seja deletado, pois já cumpriu seu objetivo. - -* **hotfix/`branch`** - * _Tudo relacionado a correções de bugs naquela branch_ - * Nomes alternativos: - * hotfix-outro_titulo/`branch` - * Apenas uma correção de bug por vez. - * Ao terminar, deve-se fazer um PR para a respectiva branch, então deletar esta aqui. - -* **feat/`branch`** - * _Tudo relacionado a novas funcionalidades naquela branch_ - * Nomes alternativos: - * feat-outro_titulo/`branch` - * Apenas uma nova funcionalidade por vez. - * Ao terminar, deve-se fazer um PR para a respectiva branch, então deletar esta aqui. - -### Github Project - -_Vulgo Board, Kanban ou Org._ - -* [Board deste projeto](https://github.com/orgs/prefeitura-rio/projects/18/views/1) - -Aqui ficam todos os problemas, correções, melhorias, e afins, que precisam ser feitos. - -Cada card representa um futuro PR a ser feito. Cada PR deve seguir o padrão adotado nas [branches do projeto](#branches-do-projeto). - -### Criando um PR - -Normalmente um PR é criado da seguinte forma: - -1. Baseado no Card do Kanban, crie uma branch a partir da branch que você está trabalhando. (e.g. de `dev` para `fix/bug1`) - -2. Subir os commits com as alterações. - -3. Criar um PR da branch nova para a original. (e.g. de `fix/bug1` para `dev`) - -No PR, preencha os seguintes campos: - -**Reviewers**: Nessa etapa verifique quem irá revisar o PR. - -**Assignees** serve para marcar quem está trabalhando no PR. (e.g. você mesmo) - -**Projects**: Marque o projeto que o PR está relacionado. - -Caso você não tenha acesso ao Project com os cards você pode fazer o seguinte: -* Peça para sua equipe te adicionar como [membro da organização](https://github.com/orgs/RJ-SMTR/people), que é diferente de [membro do projeto](https://github.com/RJ-SMTR/mobilidade-rio-api/graphs/contributors). -* Caso não seja possível, insira o link do card no PR e o link do PR no card: - - PR: - ```md - Kanban aberto em: https://github.com/orgs/prefeitura-rio/projects/18/views/1 - Nome: **[BE] Remover rotas noturnas em routes** - --- - ### Objetivo - * Tirar tudo que tiver `SN` em `route_short_name` - - ### O que foi alterado? - - * Uma configuração para filtrar rotas noturnas em settings.json - * Código para filtrar rotas noturnas na função `validate_col_values()` - - ### Como usar - ... - ``` - - Card (Kanban): - ```md - PR aberto em: https://github.com/RJ-SMTR/mobilidade-rio-api/pull/105 - --- - ### Objetivo - * Tirar tudo que tiver `SN` em `route_short_name` - - ### O que foi feito - - [x] Criar branch para hotfix de `populate_db` - - [x] Dar commit em `dev-local` - - [ ] Enviar PR - ``` - -4. Aguarde a revisão do PR. -5. Mova o card do Kanban para a coluna `✅ Feito`. - -## Problemas comuns - -### Erro ao usar manage.py - -Possíveis causas: - -**Os arquivos `migrations` foram alterados** - -Para resolver rode os comandos de acordo com o estágio em que você está trabalhando (veja [aqui](#estágios-de-desenvolvimento)): - -Dev nativo: - -* Esvaziar banco de dados - - ```sh - scripts/populate_db/populate_db.py -p 5432 --empty_db - ``` - -* Criar tabelas - - ```sh - python mobilidade_rio/manage.py migrate - ``` - -* Subir dados - - ```sh - scripts/populate_db/populate_db.py - ``` - -Dev local: - -* Eliminar arquivos do docker, e esvaziar disco virtual - - ```sh - docker-compose -f ./mobilidade_rio/dev_local/docker-compose_local.yml down -v - docker image prune -f - ``` - -* Rodar o Docker novamente - - ```sh - docker-compose -f ./mobilidade_rio/dev_local/docker-compose_local.yml up - ``` - -* Subir dados - - ```sh - python scripts/populate_db/populate_db.py - ``` -> Lembre-se que, para usar o `populate_db`, você deve ter os arquivos `.csv` na pasta `fixtures` (veja [aqui](#como-subir-dados)). - -> **O que é o disco virtual?** -> -> Disco virtual é o disco que o Docker usa para armazenar os dados do banco de dados. Com o passar do tempo, ele pode ficar cheio, ocupando centenas de GBs. -> -> Para esvaziá-lo, rode o comando `docker system prune -a -f` (ou `docker system prune -a` para ver o que será apagado). - - -### PG Admin: Erro ao conectar ao banco de dados - - -### PG Admin: must be owner of database - -**Causa:** - -O seu usuário atual (e.g. `postgres`) não é dono do banco de dados. - -**Solução:** - -1. Se necessário, peça ajuda ao responsável pelo banco de dados. - -2. Entre no banco de dados `postgres`: - -Exemplo: -```sh -psql -h localhost -p 5433 -U postgres -W postgres -``` - -3. Altere o dono do banco de dados (ex: `mobilidade_rio`) para o usuário em questão (ex: `meu_usuario`): +Execute esta query para verificar: ```sql -ALTER DATABASE mobilidade_rio OWNER TO meu_usuario; -``` \ No newline at end of file +-- Count tables +SELECT 'pontos_agency' AS table_name, COUNT(*) AS row_count FROM pontos_agency UNION ALL +SELECT 'pontos_calendar' AS table_name, COUNT(*) AS row_count FROM pontos_calendar UNION ALL +SELECT 'pontos_calendardates' AS table_name, COUNT(*) AS row_count FROM pontos_calendardates UNION ALL +SELECT 'pontos_frequencies' AS table_name, COUNT(*) AS row_count FROM pontos_frequencies UNION ALL +SELECT 'pontos_routes' AS table_name, COUNT(*) AS row_count FROM pontos_routes UNION ALL +SELECT 'pontos_shapes' AS table_name, COUNT(*) AS row_count FROM pontos_shapes UNION ALL +SELECT 'pontos_stops' AS table_name, COUNT(*) AS row_count FROM pontos_stops UNION ALL +SELECT 'pontos_stoptimes' AS table_name, COUNT(*) AS row_count FROM pontos_stoptimes UNION ALL +SELECT 'pontos_trips' AS table_name, COUNT(*) AS row_count FROM pontos_trips; +``` diff --git a/mobilidade_rio/mobilidade_rio/config_django_q/tasks.py b/mobilidade_rio/mobilidade_rio/config_django_q/tasks.py index 6ae7862..a4ff937 100644 --- a/mobilidade_rio/mobilidade_rio/config_django_q/tasks.py +++ b/mobilidade_rio/mobilidade_rio/config_django_q/tasks.py @@ -31,14 +31,19 @@ def generate_prediction(): logger.info("%i PredictorResults found, removing duplicates before save...", len(duplicated_pk)) duplicated_pk.delete() - obj, created = PredictorResult.objects.update_or_create( + obj, created_or_updated = PredictorResult.objects.update_or_create( pk=1, defaults={ "result_json": predictor_result } ) - created = "created" if created else "updated" - logger.info("new prediction %s: %s", created, obj.result_json['result'][:1]) + created_or_updated = "created" if created_or_updated else "updated" + logger.info( + "new prediction %s! length: %d, preview: %s", + created_or_updated, + len(obj.result_json['result']), + obj.result_json['result'][:1] + ) # TODO: Decide if this function will recall itself every 20 seconds 3x or if apps.py will do it. diff --git a/mobilidade_rio/mobilidade_rio/pontos/serializers.py b/mobilidade_rio/mobilidade_rio/pontos/serializers.py index 541e29b..33d5dfa 100644 --- a/mobilidade_rio/mobilidade_rio/pontos/serializers.py +++ b/mobilidade_rio/mobilidade_rio/pontos/serializers.py @@ -64,8 +64,11 @@ class Meta: fields.append("url") - class FrequenciesSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedRelatedField(view_name="frequencies-detail", read_only=True) + trip_id = TripsSerializer(read_only=True) + class Meta: model = Frequencies fields = [field.name for field in model._meta.fields] + fields.append("url") diff --git a/mobilidade_rio/mobilidade_rio/pontos/utils.py b/mobilidade_rio/mobilidade_rio/pontos/utils.py index 35aef1b..c0bf00f 100644 --- a/mobilidade_rio/mobilidade_rio/pontos/utils.py +++ b/mobilidade_rio/mobilidade_rio/pontos/utils.py @@ -1,5 +1,10 @@ +from typing import List import geopy.distance - +from django.db.models import QuerySet +from mobilidade_rio.pontos.models import ( + Frequencies, + Stops, +) def get_distance(p1: tuple, p2: tuple) -> float: return geopy.distance.great_circle(p1, p2).meters @@ -9,4 +14,39 @@ def safe_cast(val, to_type, default=None): try: return to_type(val) except (ValueError, TypeError): - return default + return default + + +def stop_times_parent_or_child( + stop_id:List[str], + queryset:QuerySet = Frequencies.objects.all().order_by("trip_id"), + ) -> QuerySet: + """ + Filter by stop_id. + + If stop is parent_station, return results from its children; + + if not, return results from itself. + """ + location_type = Stops.objects.filter( + stop_id__in=stop_id).values_list("location_type", flat=True) + + # the first stop defines if all stops will be considered parent or child + if location_type: + + # if stop is parent (station), return its children + if location_type[0] == 1: + queryset = queryset.filter( + stop_id__in=Stops.objects.filter( + parent_station__in=stop_id).values_list("stop_id", flat=True) + ) + + # if stop is child (platform), return searched stops + if location_type[0] == 0: + queryset = queryset.filter(stop_id__in=stop_id) + + # otherwise, stop id not found + else: + queryset = queryset.none() + + return queryset diff --git a/mobilidade_rio/mobilidade_rio/pontos/views.py b/mobilidade_rio/mobilidade_rio/pontos/views.py index cadbc23..fad5967 100644 --- a/mobilidade_rio/mobilidade_rio/pontos/views.py +++ b/mobilidade_rio/mobilidade_rio/pontos/views.py @@ -5,24 +5,16 @@ # stop_code import operator from functools import reduce -import django.db.models from rest_framework.exceptions import ValidationError # etc from rest_framework import viewsets from rest_framework import permissions from mobilidade_rio.pontos.models import * -import mobilidade_rio.utils.query_utils as qu from .serializers import * from .paginations import LargePagination +from .utils import stop_times_parent_or_child -# import connector to query directly from database -from django.db import connection - -cursor = connection.cursor() - -# from .utils import get_distance, safe_cast -# from .constants import constants class AgencyViewSet(viewsets.ModelViewSet): @@ -83,17 +75,26 @@ def get_queryset(self): if trip_id is not None: queryset = queryset.filter(trip_id=trip_id) - return queryset - # if code is not None: - # qrcode: QrCode = None - # try: - # qrcode: QrCode = QrCode.objects.get(stop_code=code) - # except QrCode.DoesNotExist: - # return Trip.objects.none() - # sequence: BaseManager = Stop_times.objects.filter(stop_id=qrcode.stop_id) - # queryset = queryset.filter(trip_id__in=sequence.values_list('trip_id')) + # filter trip_short_name + trip_short_name = self.request.query_params.get("trip_short_name") + if trip_short_name is not None: + trip_short_name = trip_short_name.split(',') + queryset = queryset.filter(trip_short_name__in=trip_short_name) + + # filter direction_id + direction_id = self.request.query_params.get("direction_id") + if direction_id is not None: + direction_id = direction_id.split(',') + queryset = queryset.filter(direction_id__in=direction_id) + + # filter service_id + service_id = self.request.query_params.get("service_id") + if service_id is not None: + service_id = service_id.split(',') + queryset = queryset.filter(service_id__in=service_id) + return queryset class ShapesViewSet(viewsets.ModelViewSet): @@ -181,20 +182,29 @@ def get_queryset(self): # get real col names and stuff # trip_id_col = StopTimes._meta.get_field("trip_id").column # stop_id_col = StopTimes._meta.get_field("stop_id").column - queryset = StopTimes.objects.all().order_by("trip_id") + queryset = StopTimes.objects.all().order_by("trip_id", "stop_sequence") # add parameter to show all combinations (logical OR) show_all = self.request.query_params.get("show_all") - - # filter by unique combinations (default - logical AND) + + # filter by unique trips combinations (default - logical AND) if not show_all: - unique = [ + unique_trips_fields = [ + "trip_short_name", + "direction_id", + "service_id", + "shape_id", + ] + order = [ + "trip_id", "trip_id__trip_short_name", "trip_id__direction_id", "trip_id__service_id", + "trip_id__shape_id", "stop_sequence", ] - queryset = queryset.order_by(*unique).distinct(*unique) + unique_trips = Trips.objects.order_by(*unique_trips_fields).distinct(*unique_trips_fields) + queryset = queryset.filter(trip_id__in=unique_trips).order_by(*order) # filter trip_id trip_id = self.request.query_params.get("trip_id") @@ -224,22 +234,7 @@ def get_queryset(self): stop_id = self.request.query_params.get("stop_id") if stop_id is not None: stop_id = stop_id.split(",") - location_type = Stops.objects.filter( - stop_id__in=stop_id).values_list("location_type", flat=True) - - # TODO: filter stop parent and children individually - if location_type is not None: - # if stop is parent (station), return its children - if location_type[0] == 1: - queryset = queryset.filter( - stop_id__in=Stops.objects.filter( - parent_station__in=stop_id).values_list("stop_id", flat=True) - ) - # if stop is child (platform), return searched stops - if location_type[0] == 0: - queryset = queryset.filter(stop_id__in=stop_id) - else: - queryset = queryset.none() # stop id not found + queryset = stop_times_parent_or_child(stop_id, queryset) # filter for trips passing by all given stops @@ -271,4 +266,46 @@ class FrequenciesViewSet(viewsets.ModelViewSet): serializer_class = FrequenciesSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - queryset = Frequencies.objects.all().order_by("trip_id") + queryset = Frequencies.objects.all().order_by("id") + + def get_queryset(self): + queryset = Frequencies.objects.all().order_by("id") + + # filter stop_id + stop_id = self.request.query_params.get("stop_id") + if stop_id is not None: + stop_id = stop_id.split(",") + + # filter by stop_times + stop_times = StopTimes.objects.all().order_by("trip_id", "stop_sequence") + stop_times = stop_times_parent_or_child(stop_id, stop_times) + stop_times_trip_id = list(stop_times.values_list("trip_id", flat=True)) + + # filter frequencies by + queryset = queryset.filter(trip_id__in=stop_times_trip_id) + + # filter trip_id + trip_id = self.request.query_params.get("trip_id") + if trip_id is not None: + trip_id = trip_id.split(',') + queryset = queryset.filter(trip_id__in=trip_id) + + # filter trip_short_name + trip_short_name = self.request.query_params.get("trip_short_name") + if trip_short_name is not None: + trip_short_name = trip_short_name.split(',') + queryset = queryset.filter(trip_id__trip_short_name__in=trip_short_name) + + # filter direction_id + direction_id = self.request.query_params.get("direction_id") + if direction_id is not None: + direction_id = direction_id.split(',') + queryset = queryset.filter(trip_id__direction_id__in=direction_id) + + # filter service_id + service_id = self.request.query_params.get("service_id") + if service_id is not None: + service_id = service_id.split(',') + queryset = queryset.filter(trip_id__service_id__in=service_id) + + return queryset \ No newline at end of file diff --git a/mobilidade_rio/mobilidade_rio/predictor/utils.py b/mobilidade_rio/mobilidade_rio/predictor/utils.py index c7d8967..4ed7182 100644 --- a/mobilidade_rio/mobilidade_rio/predictor/utils.py +++ b/mobilidade_rio/mobilidade_rio/predictor/utils.py @@ -8,7 +8,7 @@ from shapely.geometry import LineString, Point from shapely.ops import snap, split, transform from django.utils import timezone -from django.db.models import Q +from django.db.models import Q, QuerySet import pandas as pd from mobilidade_rio.pontos.models import ( Stops, @@ -57,32 +57,40 @@ def set_service_id(self, service_id): today_date = timezone.now().date() - # Check for date exceptions - services = CalendarDates.objects.filter(date=today_date).filter( - exception_type=1 - ) + # Get regular services + weekday = today_date.strftime("%A").lower() + regular_services_qs = Calendar.objects.filter(**{weekday: 1}) - if len(services) > 1: - raise Exception( - "Multiple services found for today. Please set a specific service_id." + # Get exception services + date_exceptions_qs = CalendarDates.objects.filter( + date=today_date, + ) + date_exceptions_add = date_exceptions_qs.filter(exception_type=1) + date_exceptions_remove = date_exceptions_qs.filter(exception_type=2) + + # Filter for regular services with date exceptions + treated_services_qs = regular_services_qs.filter( + # include services that exists as regular service between dates + Q( + start_date__lte=today_date, + end_date__gte=today_date + ) | + # or that exists as exception service at date + Q( + service_id__in=date_exceptions_add.values_list("service_id", flat=True) ) - if len(services) == 1: - return services.values_list("service_id", flat=True)[0] + # and ignore exception services to remove + ).exclude(service_id__in=(date_exceptions_remove.values_list("service_id", flat=True))) - # Check for regular service - weekday = today_date.strftime("%A").lower() - services = Calendar.objects.filter( - start_date__lte=today_date, end_date__gte=today_date - ).filter(**{weekday: 1}) + if treated_services_qs.count() > 1: + # note: Predictions for trips with different shapes may be wrong, tests are needed. + logger.info("Treated services: multiple services found for today, getting first one.") + logger.info("Services: %s",treated_services_qs.values_list("service_id", flat=True)) - if len(services) > 1: - return Exception( - "Multiple services found for today. Please set a specific service_id." - ) - if len(services) == 0: + if treated_services_qs.count() == 0: raise Exception("No service found for today.") - return services.values_list("service_id", flat=True)[0] + return treated_services_qs.values_list("service_id", flat=True) def set_realtime(self, rt_data): """ @@ -126,18 +134,29 @@ def set_realtime(self, rt_data): return data - def _get_shape_id(self, trip_short_name, direction_id, service_id): + def _get_shape_id(self, trip_short_name, direction_id, service_ids_for_today): """ - Get shape id. + Get unique shape in trips given unique (trip_short_name, direction_id) \ + + any service_id + + Parameters + --- + ``direction_id``(str): Field to filter as unique combination + + ``trip_short_name``(str): Field to filter as unique combination + + ``service_ids_for_today``(list): Any treated service_id, it must agree with date exceptions (calendar_dates) \ + or normal services (calendar) + + Return + --- + Unique shape_id for combination of fields in trips """ - # get the first trip matched, could be more - TODO: add rule to - # FE get the same trip_id (BACKLOG) # pylint: disable=W0511 - # shape_id = queryset_to_list(trips, ['shape_id']) trips = Trips.objects.filter( trip_short_name=trip_short_name, direction_id=direction_id, - service_id__in=service_id, + service_id__in=list(service_ids_for_today), ) if len(trips) == 0: @@ -259,7 +278,6 @@ def run_eta(self): if len(positions) == 0: return [] - # get the shape of the trip shape = self._get_shape_id(trip_short_name, direction_id, self.service_id) if not shape: continue @@ -270,8 +288,10 @@ def run_eta(self): # filter only stop_ids between vehicle positions # calculate ETA for all stops of the trip + stop_ids = list(StopTimes.objects.filter( - trip_id__trip_short_name=trip_short_name, trip_id__direction_id=direction_id + trip_id__trip_short_name=trip_short_name, + trip_id__direction_id=direction_id ).filter(~Q(stop_sequence=0)).values_list("stop_id", flat=True)) stops = Stops.objects.filter( stop_id__in=stop_ids, @@ -288,27 +308,3 @@ def run_eta(self): # break return result - - -# TODO: precisa manter esse __repr__? # pylint: disable=W0511 -# def __repr__(self) -> str: -# str_map_stops = "Empty" -# if not self.map_stops.empty: -# str_map_stops = f""" -# unique stop_code: {len(self.map_stops["stop_code"].unique())} -# unique trip_short_name: ({len(self.map_stops["trip_short_name"].unique())}) -# unique direction_id: {len(self.map_stops["direction_id"].unique())} -# unique shape_id: {len(self.map_stops["shape_id"].unique())} -# """ - -# return f"""\ -# Predictor values: -# input: -# stop_code: ({len(self.stop_code)}) {self.stop_code} -# trip_short_name: ({len(self.trip_short_name)}) {self.trip_short_name} -# direction_id: ({len(self.direction_id)}) {self.direction_id} -# processing: -# service_id: '{self.service_id}' -# shape_id: ({len(shape_id)}) {shape_id} -# \ -# """