<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Python |</title><link>http://ambientalanalytics.com/tag/python/</link><atom:link href="http://ambientalanalytics.com/tag/python/index.xml" rel="self" type="application/rss+xml"/><description>Python</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Mon, 22 Sep 2025 00:00:00 +0000</lastBuildDate><image><url>http://ambientalanalytics.com/media/icon_hu7c1917ae855170f5d843310bb86f2adf_19652_512x512_fill_lanczos_center_3.png</url><title>Python</title><link>http://ambientalanalytics.com/tag/python/</link></image><item><title>Struggling to Download Sentinel-2 Level 1C `.SAFE` Files with Python and AWS CLI</title><link>http://ambientalanalytics.com/post/struglingwsentinel21c/</link><pubDate>Mon, 22 Sep 2025 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/post/struglingwsentinel21c/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;blockquote>
&lt;p>I initially struggled to download the full Sentinel-2 Level 1C &lt;code>.SAFE&lt;/code> package (with metadata) for ACOLITE atmospheric correction.&lt;br>
The solution, kindly shared by my friend &lt;strong>&lt;a href="https://www.linkedin.com/in/sadeckgeo/" target="_blank" rel="noopener">Luis Sadeck&lt;/a>&lt;/strong>, was to configure &lt;strong>S3 access keys directly in my Copernicus Data Space account&lt;/strong>. &lt;br>
👉 You don’t need an AWS account for this — just generate your &lt;strong>access_key&lt;/strong> and &lt;strong>secret_key&lt;/strong> inside your &lt;strong>Data Space profile&lt;/strong>, then use them when accessing the S3 bucket.&lt;br>
Docs: &lt;a href="https://documentation.dataspace.copernicus.eu/APIs/S3.html" target="_blank" rel="noopener">Copernicus Data Space S3 API&lt;/a> &lt;br>
Now the entire process works smoothly with a single Python library. 🎉&lt;br>
Big thanks to everyone who reached out with suggestions and support!&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;p>I’ve been trying to download Sentinel-2 Level 1C data programmatically with Python, instead of manually going to the Sentinel Hub
website.
The goal is to get the entire &lt;code>.SAFE&lt;/code> package, including metadata, so I can later run atmospheric correction with ACOLITE.&lt;/p>
&lt;p>So far, here’s what I’ve tried:&lt;/p>
&lt;h2 id="attempt-1-using-the-sentinelhub-python-library">Attempt 1: Using the sentinelhub Python library&lt;/h2>
&lt;p>With the &lt;a href="https://github.com/sentinel-hub/sentinelhub-py" target="_blank" rel="noopener">sentinelhub Python package&lt;/a>, I was able to find the asset I needed.
It even provides an href pointing to the AWS S3 bucket for the &lt;code>.SAFE&lt;/code> product:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">s3://EODATA/Sentinel-2/MSI/L1C_N0500/2023/01/15/S2B_MSIL1C_20230115T133829_N0510_R124_T21HWD_20240728T040848.SAFE/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>At first, this looked promising.
But when I tried downloading the data with the AWS CLI, &lt;strong>I only got the bands and a few additional elements&lt;/strong> — not the full &lt;code>.SAFE&lt;/code> structure.&lt;/p>
&lt;p>Unfortunately, that’s a problem: &lt;strong>ACOLITE requires all metadata, not just the imagery&lt;/strong>.&lt;/p>
&lt;h2 id="attempt-2-using-stac-from-copernicus-dataspace">Attempt 2: Using STAC from Copernicus Dataspace&lt;/h2>
&lt;p>Next, I tried going through the &lt;a href="https://stac.dataspace.copernicus.eu/v1" target="_blank" rel="noopener">Copernicus Dataspace STAC API&lt;/a>.&lt;/p>
&lt;p>This was encouraging: &lt;strong>it actually lists the full &lt;code>.SAFE&lt;/code> structure, and I can see hrefs for each element&lt;/strong>. So it looked like the full dataset might be accessible.&lt;/p>
&lt;p>However, &lt;strong>when I attempted to download it using the AWS CLI, I ran into issues&lt;/strong>. Here’s the command I used:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">aws s3 cp --recursive \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> s3://eodata/Sentinel-2/MSI/L1C_N0500/2023/01/15/S2B_MSIL1C_20230115T133829_N0510_R124_T21HWD_20240728T040848.SAFE/GRANULE/L1C_T21HWD_A030609_20230115T135051/IMG_DATA/T21HWD_20230115T133829_B01.jp2 \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ./S2B_MSIL1C_20230115T133829_N0510_R124_T21HWD_20240728T040848.SAFE/ \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --request-payer requester
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Has anyone successfully managed to download the entire Sentinel-2 Level 1C &lt;code>.SAFE&lt;/code> package, including metadata, directly through Python or AWS CLI, in a way that works for running ACOLITE atmospheric correction?&lt;/strong>&lt;/p>
&lt;p>Any hints, scripts, or alternative approaches would be hugely appreciated!&lt;/p></description></item><item><title>Lessons Learned during the developtmen of `corssfire` Python package</title><link>http://ambientalanalytics.com/post/lessonslearnt/</link><pubDate>Sat, 30 Mar 2024 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/post/lessonslearnt/</guid><description>&lt;p>The &lt;a href="https://pypi.org/project/crossfire/" target="_blank" rel="noopener">crossfire&lt;/a> package was first developed in 2021, during the &lt;a href="https://felipesbarros.github.io/post/hacktoberfest21/" target="_blank" rel="noopener">#Hacktoberfest&lt;/a>. Since this first version I have recieved support from &lt;a href="https://cuducos.me/" target="_blank" rel="noopener">@cuducos&lt;/a>.
Last year, the crossfire API was updated, and thus the package should be updated as well. Again, I asked for Cuducos&amp;rsquo; help, and he kindly accepted to mentor me during the development of the new version of the package. This time, I can see that I have learned a lot of things that I would like to share here, in a not so structured form&amp;hellip;. My idea here is to write down some thoughts, notes, and comments about the process of developing the package.&lt;/p>
&lt;h2 id="debuging-in-test">Debuging in test&lt;/h2>
&lt;p>I already knew about the importance of debugging, althought I never used it. But something that I didn&amp;rsquo;t know is that I could debug the test, also.
This can be done by importing &lt;code>pytest&lt;/code> to the source code, and adding &lt;code>pytest.set_trace()&lt;/code> before the line that I want to debug. Then, we can run the test with the &lt;code>-k&lt;/code> parameter, which will run only the test that I want to debug.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">poetry&lt;/span> &lt;span class="n">run&lt;/span> &lt;span class="n">pytest&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">k&lt;/span> &lt;span class="n">test_client_load_states&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>set_trace&lt;/code> makes the test stop and leave the terminal so we can call the objects and functions to confirm if they are as expected.&lt;/p>
&lt;p>&lt;a href="https://github.com/FelipeSBarros/crossfire/pull/74#discussion_r1407879899" target="_blank" rel="noopener">here (in Pt-Br)&lt;/a>&lt;/p>
&lt;h2 id="tdd-is-about-exploring-issues-rather-than-a-solution">TDD is about exploring issues rather than a solution&lt;/h2>
&lt;p>At some point during the package development, I was facing an issue when trying to set a return value for a mocked function. After a long time analysing my code and not identifying the issue, I found in &lt;em>Stack Over Flow&lt;/em> (yes, I still using it :) a suggestion of setting the &lt;code>autospec&lt;/code> parameter to &lt;code>True&lt;/code>. Of course I looked up at the &lt;code>pytest&lt;/code> documentation to [try to] understand what does it do, but all I got was some glue about it. I decided, anyway, to use it and commit the code beliving I have solved [apparently] the issue. So, then, Cuducos adviced me:&lt;/p>
&lt;blockquote>
&lt;p>Ok, so never add code you don&amp;rsquo;t understand. Never. You can use it to ask: hey, I noticed that when I add that, it works for you. But this is a way to explore the issue rather than a solution. (&lt;a href="cuducos.me">@cuducos&lt;/a>)
&lt;a href="https://github.com/FelipeSBarros/crossfire/pull/103#discussion_r1509013934" target="_blank" rel="noopener">here&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;h2 id="flat-path-to-success">Flat path to success&lt;/h2>
&lt;p>o &lt;a href="https://www.vinta.com.br/blog/flat-success-path" target="_blank" rel="noopener">flat path to success&lt;/a> foi mencionado no processo de &lt;a href="https://github.com/FelipeSBarros/crossfire/pull/103#discussion_r1599016800" target="_blank" rel="noopener">revisão do código&lt;/a> e básicamente se refere a:&lt;/p>
&lt;blockquote>
&lt;p>escrever o código de forma clara e direta, evitando aninhamentos e complexidades, onde o propósito principal do código é facilmente discernível sem mergulhar em estruturas aninhadas.&lt;/p>
&lt;/blockquote>
&lt;h2 id="instalando-versão-pacote-python-a-partir-do-github">Instalando versão pacote Python a partir do GitHub&lt;/h2>
&lt;p>No processo de desenvolvimento é importante sempre testar o código em um ambiente diferente daquele no qual se está desenvolvendo a solução. Para isso, podemos instalar a versão do pacote diretamente do GitHub, especificando a branch a ser usada, conforme o exemplo abaixo.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install git+https://github.com/username/repo.git@branch_name
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>CROSSFIRE PYTHON CLIENT</title><link>http://ambientalanalytics.com/project/crossfire/</link><pubDate>Sat, 23 Dec 2023 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/project/crossfire/</guid><description/></item><item><title>SAILING ANALYSIS</title><link>http://ambientalanalytics.com/project/sailinganalysis/</link><pubDate>Fri, 25 Aug 2023 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/project/sailinganalysis/</guid><description/></item><item><title>DOMESTIC ECONOMY CHATBOT</title><link>http://ambientalanalytics.com/project/domesticeconomichatbot/</link><pubDate>Tue, 01 Aug 2023 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/project/domesticeconomichatbot/</guid><description/></item><item><title>Combinando Duas Paixões: Análise de Dados e Velejar.</title><link>http://ambientalanalytics.com/post/sailingdataanalysis/</link><pubDate>Wed, 14 Jun 2023 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/post/sailingdataanalysis/</guid><description>&lt;p>&lt;em>Artigo publicado também no &lt;a href="https://www.linkedin.com/pulse/combinando-duas-paix%2525C3%2525B5es-an%2525C3%2525A1lise-de-dados-e-velejar-felipe-%3FtrackingId=EAX4te6Dm9nK3%252BEixm%252F23w%253D%253D/?trackingId=EAX4te6Dm9nK3%2BEixm%2F23w%3D%3D" target="_blank" rel="noopener">linkedin&lt;/a>.&lt;/em>&lt;/p>
&lt;p>Olás! Tem algum tempo que gostaria de compartilhar com vocês um projeto pessoal, chamado &amp;ldquo;&lt;strong>Sailing Analysis&lt;/strong>&amp;rdquo;. Neste projeto, busco unir duas paixões: &lt;strong>análise de dados&lt;/strong> e &lt;strong>velejar&lt;/strong>. Através do poder da tecnologia e da ciência de dados, estou explorando maneiras de aprimorar minha performance em regatas e compreender os fatores que influenciam os resultados.&lt;/p>
&lt;p>Para dar vida ao &amp;ldquo;Sailing Analysis&amp;rdquo;, estou utilizando a linguagem de programação Python e uma variedade de bibliotecas especializadas. Vou guiar vocês através do processo que tenho seguido até agora:
1.
2. Exportação de Dados: Começo exportando os dados das minhas regatas, salvos em formato GPX, que é um padrão amplamente utilizado para representar informações de GPS, ao PostGIS, um banco de dados espacial, para armazenar e gerenciar esses dados. No entanto, caso você não esteja familiarizado com o &lt;a href="http://postgis.net/" target="_blank" rel="noopener">PostGIS&lt;/a>, também é possível trabalhar com um formato &lt;a href="https://www.geopackage.org/" target="_blank" rel="noopener">Geopackage&lt;/a>.
2. Análise de Trajetória: Utilizando a biblioteca &lt;a href="https://movingpandas.org/" target="_blank" rel="noopener">MovingPandas&lt;/a>, desenvolvido pela &lt;a href="https://www.linkedin.com/in/anita-graser-%F0%9F%8C%BB-95102530/?lipi=urn%3Ali%3Apage%3Ad_flagship3_pulse_read%3B3QvIyZu6Svaf%2BwNb7n1Rzw%3D%3D" target="_blank" rel="noopener">Anita Graser&lt;/a>, converto os pontos da rota realizada (track points) em uma estrutura de dados chamada &amp;ldquo;&lt;em>trajectory&lt;/em>&amp;rdquo;. A partir disso posso calcular diversos atributos relevantes, como aceleração, diferença angular, direção e velocidade. Essas informações me ajudam a compreender melhor o desempenho do barco em diferentes momentos da regata.
3. Persistência de Dados: Após a análise da trajetória, persisto os dados no banco de dados PostGIS ou Geopackage. Essa etapa é fundamental para manter um registro completo das regatas e permitir análises futuras.
4. Dados Meteorológicos: Como as condições meteorológicas têm um papel crucial nas regatas, integro tais informações ao meu projeto. Através de duas fontes diferentes, obtenho dados meteorológicos relevantes. Primeiro, acesso a estação meteorológica mais próxima para obter informações atualizadas a cada hora. Além disso, utilizo um modelo meteorológico para obter previsões de vento para cada posição do barco ao longo da regata.
5. Visualização e Mapeamento: Por fim, utilizo as bibliotecas Pandas, Geopandas e Matplotlib para elaborar mapas detalhados das regatas. Esses mapas me permitem visualizar toda a rota ou partes específicas das regatas, com base nas horas de início e fim, junto com as infomações de condição do vento (figura 1).&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="" srcset="
/post/sailingdataanalysis/Figura1_hufd41e48c6d135bed468e658896bc782a_633718_9c62a47409ce5be52480b0399dd2e71c.webp 400w,
/post/sailingdataanalysis/Figura1_hufd41e48c6d135bed468e658896bc782a_633718_323a616d9cd17d2c2614880821e3dc59.webp 760w,
/post/sailingdataanalysis/Figura1_hufd41e48c6d135bed468e658896bc782a_633718_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="http://ambientalanalytics.com/post/sailingdataanalysis/Figura1_hufd41e48c6d135bed468e658896bc782a_633718_9c62a47409ce5be52480b0399dd2e71c.webp"
width="760"
height="441"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>Para tornar todo o processo mais eficiente e replicável, optei por usar tecnologias como Docker, que me permite criar um ambiente de desenvolvimento isolado e fácil de configurar. Além disso, utilizo o SQLAlchemy para mapear objetos Python ao banco de dados PostGIS e o Alembic para gerenciar as migrações do banco de dados.
Quanto aos dados meteorológicos, faço uso das APIs do Open Weather Map e do Meteostat, que me fornecem acesso a informações detalhadas sobre o clima.
Este projeto tem me proporcionado uma oportunidade única de combinar minhas paixões pessoais com habilidades técnicas. Através da análise de dados, estou ganhando insights valiosos sobre o meu desempenho na vela, identificando áreas de melhoria e descobrindo padrões que podem me ajudar a obter melhores resultados.
No futuro, planejo expandir o &amp;ldquo;Sailing Analysis&amp;rdquo; e compartilhar minhas descobertas com outros velejadores. Acredito que a análise de dados pode beneficiar toda a comunidade náutica, proporcionando uma compreensão mais profunda das estratégias de regata, das condições ideais de vento e de outros fatores importantes.
Se você também é apaixonado por velejar ou análise de dados, adoraria trocar experiências e ideias com você. Vamos nos conectar e impulsionar o potencial dessa combinação fascinante!
Para saber mais sobre o projeto &amp;ldquo;Sailing Analysis&amp;rdquo; e acompanhar o seu desenvolvimento, visite o repositório no GitHub: &lt;a href="https://github.com/FelipeSBarros/SailingAnalysis" target="_blank" rel="noopener">Sailing Analysis&lt;/a>.&lt;/p>
&lt;p>Abraço e até breve!&lt;/p></description></item><item><title>Aprendendo sobre datetime, SQLAlchemy e PostgreSQL a partir de bugs</title><link>http://ambientalanalytics.com/post/aprendendodatetime/</link><pubDate>Wed, 15 Jun 2022 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/post/aprendendodatetime/</guid><description>&lt;p>Há algum tempo comecei a perceber um “comportamento estranho” (ainda que tenha colocado o termo bug no título, acho que não é o caso. Foi para atrir mais atenção, mesmo:) relacionado aos dados de data e hora num sistema que estava desenvolvendo. Minha reação inicial, praticamente um instinto de sobrevivência, foi simplesmente resolver a situação contornando o problema. Mas chegou um momento que precisei entender a origem do mesmo. Mais uma vez tive que fazer um exercício de seguir/isolar o problema que me assombrava (&lt;a href="https://felipesbarros.github.io/post/aprendendo-sobre-datetime-sqlalchemy-e-postgresql-a-partir-de-bugs/felipesbarros.github.io" target="_blank" rel="noopener">veja outros artigos que produzi sobre bugs/comportamentos estranhos&lt;/a>) para tentar compreender o motivo da sua existência. Esse processo tomou-me alguns dias e, claro, proporcionou alguns aprendizados.&lt;/p>
&lt;p>Ainda que agora, tendo resolvido e entendido as causas e origens desse comportamento, tudo parece óbvio, decidi compartilhar um pouco deste processo, pois nessa busca por soluções não encontrei nada que me ajudasse de forma objetiva.&lt;/p>
&lt;p>Criei um ambiente para reproduzir esses “comportamentos estranhos” (&lt;a href="https://felipesbarros.github.io/post/aprendendo-sobre-datetime-sqlalchemy-e-postgresql-a-partir-de-bugs/#Preparando-ambiente-de-desenvolvimento" target="_blank" rel="noopener">há uma seção sobre como preparar um ambiente para poder reproduzir esses códigos&lt;/a>) e deixarei os trechos de códigos usados para vocês poderem reproduzir os passos dados. Irei trabalhar em todos os exemplos com um mesmo objeto de data e hora (instância &lt;code>datetime&lt;/code>) mudando apenas o uso de fuso horário, para torná-los conscientes (aware) ou não (naive, ingênuo) (leia um pouco sobre isso &lt;a href="https://docs.python.org/3/library/datetime.html#aware-and-naive-objects" target="_blank" rel="noopener">aqui&lt;/a>). Na seção final, “resumo”, deixo os principais aprendizados deste processo.&lt;/p>
&lt;h2 id="contextualizando-o-sistema">Contextualizando o sistema&lt;/h2>
&lt;p>Antes de tudo, lhes resumo a parte que importa do sistema:&lt;/p>
&lt;p>O mesmo estava em um servidor com fuso horário UTC, e nele eu manipulava um dado de data e hora, usando o módulo python &lt;a href="https://docs.python.org/3/library/datetime.html" target="_blank" rel="noopener">&lt;code>datetime&lt;/code>&lt;/a>, com time zone consciente (&lt;em>aware&lt;/em>), transformando-os ao time zone de Brasília (-0300). Esse dado era, então, persistido no banco de dados &lt;a href="https://www.postgresql.org/" target="_blank" rel="noopener">PostgreSQL&lt;/a>, que estava em outro servidor, também com fuso horário UTC. Os dados eram persistidos em duas colunas diferentes: uma coluna &lt;a href="https://www.postgresql.org/docs/current/datatype-datetime.html" target="_blank" rel="noopener">DateTime com time zone consciente&lt;/a> e numa coluna de texto onde, além da data e hora em formato &lt;a href="https://docs.python.org/3/library/datetime.html#datetime.date.isoformat" target="_blank" rel="noopener">iso&lt;/a>, uma observação textual era adicionada (que não vem ao caso, agora). Mas é importante saber que tínhamos o mesmo dado de data e hora persistido como tal e como texto.&lt;/p>
&lt;p>Um detalhe não menos importante é o fato de eu estar usando o módulo &lt;a href="https://pythonhosted.org/pytz/" target="_blank" rel="noopener">&lt;code>pytz&lt;/code>&lt;/a> para definir o fuso &lt;code>America/Sao_Paulo&lt;/code>, e o &lt;a href="https://www.sqlalchemy.org/" target="_blank" rel="noopener">SQLAlchemy&lt;/a>, para fazer a conexão com o banco de dados, commit e etc. Pensando em facilitar a minha vida, estive usando o &lt;a href="https://dbeaver.io/" target="_blank" rel="noopener">DBeaver&lt;/a>, uma interface gráfica para gestão de banco de dados. Ou seja, usava o DBeaver para conectar ao banco de dados e observar o que estava sendo persistido sem precisar fazê-lo pelo &lt;a href="https://www.postgresql.org/docs/current/app-psql.html" target="_blank" rel="noopener">&lt;code>psql&lt;/code>&lt;/a>.&lt;/p>
&lt;h2 id="reproduzindo-comportamentos-estranhos">Reproduzindo comportamentos estranhos&lt;/h2>
&lt;p>Basicamente criei uma instância &lt;code>datetime&lt;/code> ingênua (&lt;em>naive&lt;/em>) em relação ao time zone e outra com time zone declarado ( consciente, &lt;em>aware&lt;/em>). Criei uma instância da tabela persistindo cada dado nas suas respectivas colunas (consciente na coluna consciente e ingênuo na coluna ingênua) (&lt;a href="https://felipesbarros.github.io/post/aprendendo-sobre-datetime-sqlalchemy-e-postgresql-a-partir-de-bugs/#Preparando-ambiente-de-desenvolvimento" target="_blank" rel="noopener">veja aqui sobre a criação do ambiente para reproduzir esses códigos&lt;/a>).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">pytz&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">datetime&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">datetime&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">sqlalchemy&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">create_engine&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">sqlalchemy.orm&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">sessionmaker&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">engine&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">create_engine&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;postgresql+psycopg2://postgres:password@localhost:5432/postgres&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Session&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sessionmaker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">bind&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">engine&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Session&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BR_TIME_ZONE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pytz&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">timezone&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;America/Sao_Paulo&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">naive&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2022&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">27&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">aware&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">naive&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tzinfo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">BR_TIME_ZONE&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">record&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">DateTimeTable&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date_time_tz_aware&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">aware&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">isoformat_tz_aware&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">aware&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isoformat&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date_time_naive&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">naive&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">isoformat_naive&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">naive&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isoformat&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">commit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ao fazer o commit e consultar a base de dados, começa o terror e pânico:&lt;/p>
&lt;p>Usando o DBeaver para acessar o registro criado (seja pela interface gráfica como pela query da GUI), observei que:&lt;/p>
&lt;ul>
&lt;li>O valor persistido na coluna consciente foi alterado em seis minutos (acrescidos). Deveria ser 12:30 e passou a ser 12:36, ao passo que a informação de time zone é apresentada de forma correta: &lt;code>-0300&lt;/code>;&lt;/li>
&lt;li>O dado da coluna &lt;code>iso_format_tz_aware&lt;/code> possui a informação sem qualquer alteração. Ao passo que a time zone informada não é a esperada (&lt;code>-0300&lt;/code>), mas &lt;code>-03:06&lt;/code>. Lembrem-se que o time zone da coluna &lt;code>date_time_aware&lt;/code> é informado apenas &lt;code>-0300&lt;/code>;&lt;/li>
&lt;li>Os dados persistidos nos campos time zone ingênuos não apresentaram qualquer alteração.&lt;/li>
&lt;/ul>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>id&lt;/th>
&lt;th>date_time_tz_aware&lt;/th>
&lt;th>iso_format_tz_aware&lt;/th>
&lt;th>date_time_naive&lt;/th>
&lt;th>isofomat_naive&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2022-05-27 12:36:00.000 -0300&lt;/td>
&lt;td>2022-05-27T12:30:00-03:06&lt;/td>
&lt;td>2022-05-27 12:30:00.000&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Contudo, ao acessar esses dados usando o SQLAlchemy, a confusão aumenta:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>id&lt;/th>
&lt;th>date_time_tz_aware&lt;/th>
&lt;th>iso_format_tz_aware&lt;/th>
&lt;th>date_time_naive&lt;/th>
&lt;th>isofomat_naive&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2022-05-27 15:36:00+00:00&lt;/td>
&lt;td>2022-05-27T12:30:00-03:06&lt;/td>
&lt;td>2022-05-27 12:30:00&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Reparem que agora temos:&lt;/p>
&lt;ul>
&lt;li>Na coluna &lt;code>date_time_tz_aware&lt;/code>, o objeto tem três horas e seis minutos acrescidos e o time zone informado como UTC (&lt;code>+00:00&lt;/code>).&lt;/li>
&lt;li>Os dados das colunas &lt;code>iso_format&lt;/code>, &lt;code>date_time_naive&lt;/code> e &lt;code>isoformat_naive&lt;/code> apresentam os dados assim como estão no banco de dados.&lt;/li>
&lt;/ul>
&lt;p>Comportamentos estranhos a serem resolvidos:&lt;/p>
&lt;ul>
&lt;li>O &lt;em>time zone&lt;/em> deveria ser de &lt;code>-0300&lt;/code>. &lt;strong>De onde veio os seis minutos a mais?&lt;/strong>&lt;/li>
&lt;li>Afinal, o dado é persistido no banco de dados em UTC (como retornado pelo SQLAlchemy) ou no fuso horário informado no objeto datetime (como retornado pelo DBeaver)?&lt;/li>
&lt;/ul>
&lt;h2 id="resolvendo-problema-de-definição-de-time-zone">Resolvendo problema de definição de time zone&lt;/h2>
&lt;p>Ao apresentar esses problemas aos amigos que tenho como referência na área, um deles, o &lt;a href="https://twitter.com/georgersilva" target="_blank" rel="noopener">@georgersilva&lt;/a>, me alertou que a forma como eu estava definido o time zone estava equivocado. A única direção dada por ele foi &lt;a href="https://stackoverflow.com/questions/1379740/pytz-localize-vs-datetime-replace" target="_blank" rel="noopener">essa pergunta no Stack Overflow&lt;/a>.&lt;/p>
&lt;p>Um comentário me chamou a atenção:&lt;/p>
&lt;blockquote>
&lt;p>@MichaelWaterfall: pytz.timezone() may correspond to several tzinfo objects (same place, different UTC offsets, time zone abbreviations). tz.localize(d) tries to find the correct tzinfo for the given d local time (some local time is ambiguous or doesn’t exist). replace() just sets whatever (random) info pytz time zone provides by default without regard for the given date (LMT in recent versions). tz.normalize() may adjust the time if d is a non-existent local time e.g., the time during DST transition in Spring (northern hemisphere) otherwise it does nothing in this case.&lt;/p>
&lt;/blockquote>
&lt;p>Em tradução livre:&lt;/p>
&lt;blockquote>
&lt;p>pytz.timezone() pode corresponder a objetos com diferentes tzinfo (mesmo local, diferentes offset em relação ao UTC). tz.localize(d) tenta encontrar o tzinfo correto para um dada hora local (algumas horas locais são ambíguas ou inexistentes). replace() apenas define qualquer informação de time zone por padrão sem se preocupar com a data. tz.normalize() deve ajustar a informação de tempo se o objeto d não possuir informação de hora local.&lt;/p>
&lt;/blockquote>
&lt;p>Como estou usando o &lt;code>pytz&lt;/code> para definir um objeto de data com fuso horário, o &lt;code>replace&lt;/code> não seria a forma correta, mas sim, o método &lt;code>localize&lt;/code> da própria instância &lt;code>pytz.timezone&lt;/code>.&lt;/p>
&lt;p>Vamos testar, então:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">BR_TIME_ZONE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pytz&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">timezone&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;America/Sao_Paulo&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">naive&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2022&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">27&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">naive&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tzinfo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">BR_TIME_ZONE&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># datetime.datetime(2022, 5, 27, 12, 30, tzinfo=&amp;lt;DstTzInfo &amp;#39;America/Sao_Paulo&amp;#39; LMT-1 day, 20:54:00 STD&amp;gt;)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BR_TIME_ZONE&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">localize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">naive&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># datetime.datetime(2022, 5, 27, 12, 30, tzinfo=&amp;lt;DstTzInfo &amp;#39;America/Sao_Paulo&amp;#39; -03-1 day, 21:00:00 STD&amp;gt;)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reparem a diferença que isso fez no parâmetro &lt;code>tzinfo&lt;/code> da instância: há uma diferença de seis minutos no objeto ao qual usei o método &lt;code>replace()&lt;/code>. Ao usar o &lt;code>localize()&lt;/code>, a informação de fuso horário “-03” aparece.&lt;/p>
&lt;p>Fiz mais um teste para entender se o problema é o método &lt;code>replace&lt;/code> ou a forma como o &lt;code>pytz&lt;/code> define o time zone:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2022&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">27&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tzinfo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">BR_TIME_ZONE&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># datetime.datetime(2022, 5, 27, 12, 30, tzinfo=&amp;lt;DstTzInfo &amp;#39;America/Sao_Paulo&amp;#39; LMT-1 day, 20:54:00 STD&amp;gt;)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mesmo passando o time zone do &lt;code>pytz&lt;/code> como parâmetro &lt;code>tzinfo&lt;/code>, a diferença de seis minutos segue (20:54). Ou seja, também não seria a forma correta.&lt;/p>
&lt;p>Na documentação do &lt;code>localize&lt;/code>, há apenas a menção:&lt;/p>
&lt;blockquote>
&lt;p>Unfortunately using the tzinfo argument of the standard datetime constructors ‘’does not work’’ with pytz for many timezones.&lt;/p>
&lt;/blockquote>
&lt;p>Ao salvar no banco de dados o objeto &lt;code>aware&lt;/code> criado usando o &lt;code>localize&lt;/code>, os dados foram, enfim, salvos de forma correta:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>id&lt;/th>
&lt;th>date_time_tz_aware&lt;/th>
&lt;th>iso_format_tz_aware&lt;/th>
&lt;th>date_time_naive&lt;/th>
&lt;th>isofomat_naive&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2022-05-27 12:30:00.000 -0300&lt;/td>
&lt;td>2022-05-27T12:30:00-03:00&lt;/td>
&lt;td>2022-05-27 12:30:00.000&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>✔️ OK, um problema resolvido. Gracias, &lt;a href="https://twitter.com/georgersilva" target="_blank" rel="noopener">@georgersilva&lt;/a>!&lt;br>
❓ Mas ainda fica o mistério das conversões entre o dado acessado pelo DBeaver daquele acessado pelo SQLAlchemy.&lt;/p>
&lt;p>Enquanto estava tentando resolver esse segundo problema, o &lt;a href="https://twitter.com/dunossauro" target="_blank" rel="noopener">@dunossauro&lt;/a> fez uma &lt;a href="https://youtu.be/BImF-dZYass?t=3948" target="_blank" rel="noopener">live de python sobre &lt;code>datetime&lt;/code>&lt;/a>. Fui assistir e vi que ele indicou usarmos a definição de time zone usando &lt;a href="https://docs.python.org/3/library/datetime.html#timedelta-objects" target="_blank" rel="noopener">&lt;code>timedelta&lt;/code>&lt;/a>. Me pareceu sensato.&lt;/p>
&lt;p>Vamos testar, então:&lt;/p>
&lt;h2 id="resolvendo-problema-de-definição-de-time-zone-com-timedelta">Resolvendo problema de definição de time zone com &lt;code>timedelta&lt;/code>&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">datetime&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">timezone&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">timedelta&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># BR_TIME_ZONE = pytz.timezone(&amp;#34;America/Sao_Paulo&amp;#34;)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BR_TIME_ZONE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">timezone&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">timedelta&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hours&lt;/span>&lt;span class="o">=-&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2022&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">27&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tzinfo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">BR_TIME_ZONE&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># datetime.datetime(2022, 5, 27, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=75600)))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2022&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">27&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tzinfo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">BR_TIME_ZONE&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># datetime.datetime(2022, 5, 27, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=75600)))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reparem que agora não estamos mais usando uma instância &lt;code>timezone&lt;/code> do &lt;code>pytz&lt;/code> e por isso não podemos usar o método &lt;code>localize()&lt;/code>. Com o &lt;code>timedelta&lt;/code>, obtivemos os resultados esperados tanto passando o objeto no parâmetro &lt;code>tzinfo&lt;/code>, na criação da instância &lt;code>datetime&lt;/code>, como ao usar o método &lt;code>replace&lt;/code>.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>id&lt;/th>
&lt;th>date_time_tz_aware&lt;/th>
&lt;th>iso_format_tz_aware&lt;/th>
&lt;th>date_time_naive&lt;/th>
&lt;th>isofomat_naive&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2022-05-27 12:30:00.000 -0300&lt;/td>
&lt;td>2022-05-27T12:30:00-03:00&lt;/td>
&lt;td>2022-05-27 12:30:00.000&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Dessa forma também temos os dados persistidos corretamente e ainda nos poupa de usar o &lt;code>pytz&lt;/code>. Gracias, &lt;a href="https://twitter.com/dunossauro" target="_blank" rel="noopener">@dunossauro&lt;/a>!&lt;/p>
&lt;h2 id="o-mistério-das-consultas-sendo-retornadas-em-utc-e--0300">O mistério das consultas sendo retornadas em UTC e -0300&lt;/h2>
&lt;p>Só para refrescar a memória: Ao acessar os dados persistidos no banco de dados usando o DBeaver, os recebia com o time zone -0300, enquanto ao acessar pelo SQLAlchemy, os mesmos dados eram retornados em UTC +00:00.&lt;/p>
&lt;p>Decidi acessar o banco e fazer as consultas apresentadas anteriormente pelo &lt;a href="https://www.postgresqlql.org/docs/current/app-psql.html" target="_blank" rel="noopener">psql&lt;/a> e pelo DBeaver para confirmar:&lt;/p>
&lt;ul>
&lt;li>o fuso horário da instância do banco de dados, e;&lt;/li>
&lt;li>o fuso horário dos dados persistidos;&lt;/li>
&lt;/ul>
&lt;h3 id="confirmando-o-fuso-horário-da-instância-do-banco-de-dados">Confirmando o fuso horário da instância do banco de dados&lt;/h3>
&lt;p>Executando o mesmo comando no &lt;code>psql&lt;/code> e DBeaver para uma mesma instância de banco dados tive diferentes retornos:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">psql -h localhost -U postgres -p 5432 postgres
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">show timezone;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="" srcset="
/post/aprendendodatetime/show_timezone_psql_hu2ddcf8b2ea2c34604b69bb060fc6c1d4_77562_c6f564837a28302f69f543598e078c72.webp 400w,
/post/aprendendodatetime/show_timezone_psql_hu2ddcf8b2ea2c34604b69bb060fc6c1d4_77562_7fc7a919731e8be5db1aeab8e39b2f0b.webp 760w,
/post/aprendendodatetime/show_timezone_psql_hu2ddcf8b2ea2c34604b69bb060fc6c1d4_77562_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="http://ambientalanalytics.com/post/aprendendodatetime/show_timezone_psql_hu2ddcf8b2ea2c34604b69bb060fc6c1d4_77562_c6f564837a28302f69f543598e078c72.webp"
width="745"
height="403"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="" srcset="
/post/aprendendodatetime/show_timezone_dbeaver_hue9ab32970e4e2c19fb3254c44c7a3f58_47343_0183aa30924032d064e137398bd54854.webp 400w,
/post/aprendendodatetime/show_timezone_dbeaver_hue9ab32970e4e2c19fb3254c44c7a3f58_47343_ee04fb9cba235332b742da876a67473b.webp 760w,
/post/aprendendodatetime/show_timezone_dbeaver_hue9ab32970e4e2c19fb3254c44c7a3f58_47343_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="http://ambientalanalytics.com/post/aprendendodatetime/show_timezone_dbeaver_hue9ab32970e4e2c19fb3254c44c7a3f58_47343_0183aa30924032d064e137398bd54854.webp"
width="760"
height="213"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>Eis, então, que fica evidente: A ferramenta usada para conexão e consulta ao banco de dados é que foram as responsáveis pelas diferenas observadas no time zone.&lt;/p>
&lt;p>Isso me fez lembrar da documentação do PostgreSQL que já havia lido, mas não tinha dado a devida atenção:&lt;/p>
&lt;blockquote>
&lt;p>For timestamp with time zone, the internally stored value is always in UTC (Universal Coordinated Time, traditionally known as Greenwich Mean Time, GMT). An input value that has an explicit time zone specified is converted to UTC using the appropriate offset for that time zone. If no time zone is stated in the input string, then it is assumed to be in the time zone indicated by the system’s time zone parameter, and is converted to UTC using the offset for the time zone zone. &lt;a href="https://www.postgresql.org/docs/current/datatype-datetime.html" target="_blank" rel="noopener">fonte&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;p>Em tradução livre:&lt;/p>
&lt;blockquote>
&lt;p>Para dados com informação de time zone, o valor armazenado estará sempre em UTC (também conhecido como GMT). Um valor de entrada que não tenha time zone declarado explicitamente será convertido a UTC usando o time zone indicado pelo sistema.&lt;/p>
&lt;/blockquote>
&lt;p>A partir disso, várias constatações:&lt;/p>
&lt;ul>
&lt;li>Os dados que possuem a informação de time zone, são convertidos a UTC. Os dados sem essa definição é entendido como já estando em UTC, logo não é convertido.&lt;/li>
&lt;li>O DBeaver identificou o time zone da minha máquina e ao retornar uma consulta já convertia todos os dados considerando o time zone da minha maquina.&lt;/li>
&lt;li>Não é o SQLAlchemy que define como os dados serão resgatados, mas o PostgreSQL. Na verdade, essa definição é feita pela seção de conexão com o banco de dados.&lt;/li>
&lt;/ul>
&lt;p>Vejam:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">psql -h localhost -U postgres -p 5432 postgres
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PostgreSQL= show time zone;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># time zone
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># ----------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># Etc/UTC
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># (1 row)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">select * from datetime;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Com uma seção (conexão) recém iniciada, o time zone é configurado para UTC (padrão), com os dados sendo retornados em UTC.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>id&lt;/th>
&lt;th>date_time_tz_aware&lt;/th>
&lt;th>iso_format_tz_aware&lt;/th>
&lt;th>date_time_naive&lt;/th>
&lt;th>isofomat_naive&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2022-05-27 15:36:00+00&lt;/td>
&lt;td>2022-05-27T12:30:00-03:06&lt;/td>
&lt;td>2022-05-27 15:36:00&lt;/td>
&lt;td>2022-05-27T12:30:00-03:06&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>2022-05-27 12:30:00+00&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;td>2022-05-27 12:30:00&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Se, na mesma conexão, eu configuro o time zone para &lt;code>America/Sao_Paulo&lt;/code>, e executo a mesma query, os dados na coluna com time zone consciente serão apresentados convertidos ao time zone definido na conexão (&lt;code>-0300&lt;/code>).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">postgres=set timezone = &amp;#39;America/Sao_Paulo&amp;#39;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">#SET
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">postgres=show timezone;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># timezone
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># -------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># America/Sao_Paulo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># (1 row)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">select * from datetime;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Eis que todos os dados são retornados em &lt;code>-0300&lt;/code>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>id&lt;/th>
&lt;th>date_time_tz_aware&lt;/th>
&lt;th>iso_format_tz_aware&lt;/th>
&lt;th>date_time_naive&lt;/th>
&lt;th>isofomat_naive&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2022-05-27 12:36:00-03&lt;/td>
&lt;td>2022-05-27T12:30:00-03:06&lt;/td>
&lt;td>2022-05-27 12:30:00&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>2022-05-27 09:30:00-03&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;td>2022-05-27 12:30:00&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Tudo parece bem óbvio, não? Mas uma coisa que foi fundamental para a minha confusão mental sobre esse comportamento: o fato de estar usando o DBeaver os dados eram apresentados já convertidos para a time zone do meu sistema e, com isso, eu acreditava que os mesmos estavam sendo persistidos como tal no banco de dados. Ao acessar os dados pelo SQLAlchemy (que usa uma seção padrão, sem configuração de time zone, logo em UTC) recebia os dados em UTC. Ficando sem entender o que, de fato, estava sendo persistido.&lt;/p>
&lt;p>:heavy_checkmark: Fica o aprendizado: O PostgreSQL irá retornar os dados de data e hora no time zone da seção de conexão, que por padrão é UTC. Caso vc queira receber-los em outro time zone, basta definir usando o &lt;code>SET timezone&lt;/code>, ou, se for usando o SQLAlchemy, você poderá fazê-lo usando o parâmetro &lt;a href="https://docs.sqlalchemy.org/en/14/core/engines.html#sqlalchemy.create_engine.params.connect_args" target="_blank" rel="noopener">&lt;code>connect_args&lt;/code>&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">engine&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">create_engine&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">connect_args&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;options&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;-c timezone=-3&amp;#34;&lt;/span>&lt;span class="p">})&lt;/span>&lt;span class="err">`&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="coluna-naive-e-aware">coluna naive e aware&lt;/h2>
&lt;p>Ainda que tenha tomado um tempo considerável resolução de todas essas dúvidas, não chegou a esgotar a minha paciência. Por isso, fiz mais alguns testes, para tentar entender, de vez, a diferença entre usar ou não coluna com time zone consciente e ingênua no PostgreSQL.&lt;/p>
&lt;p>Ainda que já esteja superada a dúvida sobre as diferenças entre DBeaver e SQLAlchemy, seguirei apresentando as consultas usando ambas ferramentas, pois isso nos ajudará a entender as consequências ao usar campo consciente ou ingênuo.&lt;/p>
&lt;h3 id="primeiro-teste">Primeiro teste:&lt;/h3>
&lt;p>Inseri em ambos campos de datetime (consciente e ingênuo), um objeto com time zone consciente:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">record&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">DateTimeTable&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date_time_tz_aware&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">aware&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">isoformat_tz_aware&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">aware&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isoformat&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date_time_naive&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">aware&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">isoformat_naive&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">aware&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isoformat&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">commit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ao fazê-lo, o PosgreSQL entenderá o time zone do dado e, como dito anteriormente, os persitirá em UTC (logo, acrescentando três horas). &lt;strong>Isso tanto para o campo consciente como para o campo ingênuo&lt;/strong>. A diferença, contudo estará no resgate da informação por uma seção em UTC ou em outro time zone:&lt;/p>
&lt;p>Acessando esse dado pelo DBeaver (seção com time zone configurado em -0300), tenho o valor do campo &lt;code>aware&lt;/code> convertido ao time zone da seção (-0300) e indicando o mesmo, ao passo que o valor persistido no campo &lt;code>naive&lt;/code> se mantêm em formato UTC e sem a indicação do time zone:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>id&lt;/th>
&lt;th>date_time_tz_aware&lt;/th>
&lt;th>iso_format_tz_aware&lt;/th>
&lt;th>date_time_naive&lt;/th>
&lt;th>isofomat_naive&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2022-05-27 12:30:00.000 -0300&lt;/td>
&lt;td>2022-05-27T12:30:00-03:00&lt;/td>
&lt;td>2022-05-27 15:30:00.000&lt;/td>
&lt;td>2022-05-27T12:30:00-03:00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>á pelo SQLAlchemy a informação persistida no campo time zone consciente é retornada respeitando o time zone da seção (logo, time zone UTC) e no campo ingênuo, não há alteração.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>id&lt;/th>
&lt;th>date_time_tz_aware&lt;/th>
&lt;th>iso_format_tz_aware&lt;/th>
&lt;th>date_time_naive&lt;/th>
&lt;th>isofomat_naive&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2022-05-27 15:30:00+00:00&lt;/td>
&lt;td>2022-05-27T12:30:00-03:00&lt;/td>
&lt;td>2022-05-27 15:30:00&lt;/td>
&lt;td>2022-05-27T12:30:00-03:00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="segundo-teste">Segundo teste:&lt;/h3>
&lt;p>Ao inserir em ambos campos, um objeto ingênuo, o PostgreSQL entende que os mesmos já estão em UTC. Logo, ao acessá-los pelo DBeaver (seção com time zone -0300), o valor no campo consciente apresenta o desconto de três horas e apresenta a informação de time zone -0300, e na coluna &lt;code>naive&lt;/code>, os valores não são alterados.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># terceiro registro inserindo datetime naive sempre&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">record&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">DateTimeTable&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date_time_tz_aware&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">naive&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">isoformat_tz_aware&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">naive&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isoformat&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date_time_naive&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">naive&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">isoformat_naive&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">naive&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isoformat&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">commit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>id&lt;/th>
&lt;th>date_time_tz_aware&lt;/th>
&lt;th>iso_format_tz_aware&lt;/th>
&lt;th>date_time_naive&lt;/th>
&lt;th>isofomat_naive&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2022-05-27 09:30:00.000 -0300&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;td>2022-05-27 12:30:00.000&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Acessando so dados pelo SQLAlchemy, tanto a coluna consciente como a ingênua apresentam o mesmo valor. Contudo, no campo consciente, a informação do time zone é existente (+00:00:00).&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>id&lt;/th>
&lt;th>date_time_tz_aware&lt;/th>
&lt;th>iso_format_tz_aware&lt;/th>
&lt;th>date_time_naive&lt;/th>
&lt;th>isofomat_naive&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2022-05-27 12:30:00+00:00&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;td>2022-05-27 12:30:00&lt;/td>
&lt;td>2022-05-27T12:30:00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="preparando-ambiente-de-desenvolvimento">Preparando ambiente de desenvolvimento&lt;/h2>
&lt;p>Para isolar e reproduzir os comportamentos apresentados segui os seguintes passos:&lt;/p>
&lt;ol>
&lt;li>Criação de um ambiente virtual;&lt;/li>
&lt;li>Activação do ambiente virtual;&lt;/li>
&lt;li>Atualização do pip;&lt;/li>
&lt;li>Instalação dos pacotes listados em &lt;a href="https://felipesbarros.github.io/post/aprendendo-sobre-datetime-sqlalchemy-e-postgresql-a-partir-de-bugs/requirements.txt" target="_blank" rel="noopener">requirements.txt&lt;/a>;&lt;/li>
&lt;li>Criei uma instância do banco de dados &lt;a href="https://felipesbarros.github.io/post/aprendendo-sobre-datetime-sqlalchemy-e-postgresql-a-partir-de-bugs/#Docker-com-PostgreSQL" target="_blank" rel="noopener">PostgreSQL usando docker&lt;/a>;&lt;/li>
&lt;li>Criei os modelos as tabelas usando &lt;a href="https://felipesbarros.github.io/post/aprendendo-sobre-datetime-sqlalchemy-e-postgresql-a-partir-de-bugs/#Modelo-de-dados-e-conex%c3%a3o-com-SQLAlchemy" target="_blank" rel="noopener">SQLAlchemy&lt;/a>;&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">mkdir datetime
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cd datetime
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python -m venv .venv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">source .venv/bin/activate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip intall --upgrade pip
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install -r requirements.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="docker-com-postgresql">Docker com PostgreSQL&lt;/h3>
&lt;p>Para facilitar, criei uma instância Docker com a imagem original do PostgreSQLQL. Caso já o tenha instalado em sua máquina, desconsidere.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">docker pull PostgreSQL
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run --name teste_datetime -e PostgreSQL_PASSWORD=password -d PostgreSQL
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># confirmando existencia
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker container ps
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">#CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">#c77150c506a8 PostgreSQL &amp;#34;docker-entrypoint.s…&amp;#34; 6 seconds ago Up 5 seconds
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="modelo-de-dados-e-conexão-com-sqlalchemy">Modelo de dados e conexão com SQLAlchemy&lt;/h3>
&lt;p>Crio, em um arquivo &lt;code>models.py&lt;/code>, a classe que representará a tabela &lt;code>datetime&lt;/code> do banco de dados. Nela teremos os campos &lt;code>date_time_tz_aware&lt;/code>, &lt;code>date_time_naive&lt;/code> que são, ambos, &lt;a href="https://docs.sqlalchemy.org/en/14/core/type_basics.html#sqlalchemy.types.DateTime" target="_blank" rel="noopener">&lt;code>DateTime()&lt;/code>&lt;/a>, com o parâmetro &lt;code>timezone=True&lt;/code> (&lt;strong>verdadeiro&lt;/strong> e &lt;strong>falso&lt;/strong>, respectivamente). Os campos &lt;code>isoformat_tz_aware&lt;/code> e &lt;code>isoformat_naive&lt;/code> serão os campos textuais que persistirão os dados de data e hora em formato &lt;a href="https://docs.python.org/3/library/datetime.html#datetime.datetime.isoformat" target="_blank" rel="noopener">&lt;code>isoformat()&lt;/code>&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># models.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">sqlalchemy&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Integer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">DateTime&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Text&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">sqlalchemy&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">create_engine&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Column&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">sqlalchemy.ext.declarative&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">declarative_base&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Base&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">declarative_base&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BD_USERNAME&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;PostgreSQL&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BD_PASSWORD&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;password&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BD_HOST&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;localhost&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BD_PORT&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;5433&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BD_NAME&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;PostgreSQL&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">db_connect&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">create_engine&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;PostgreSQLql+psycopg2://&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">BD_USERNAME&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">BD_PASSWORD&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">@&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">BD_HOST&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">BD_PORT&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">BD_NAME&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">create_table&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">engine&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Base&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create_all&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">engine&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">DateTimeTable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Base&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">__tablename__&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;datetime&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Column&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Integer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">primary_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date_time_tz_aware&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Column&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">DateTime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">timezone&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">isoformat_tz_aware&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Column&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Text&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">date_time_naive&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Column&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;datetime_naive&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">DateTime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">timezone&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">isoformat_naive&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Column&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Text&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">engine&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">db_connect&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">create_table&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">engine&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="identificando-time-zone-das-instâncias-de-trabalho">Identificando time zone das instâncias de trabalho&lt;/h3>
&lt;p>Para confirmar que estamos reproduzindo as mesmas situações, vamos confirmar o time zone da base de dados.&lt;/p>
&lt;h4 id="docker-postgresql">Docker PostgreSQL&lt;/h4>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">psql -h localhost -U PostgreSQL -p 5433
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">show timezone;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># timezone
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">#----------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># Etc/UTC
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">#(1 row)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ao executar a consulta select now(), ele me dá a data e hora com a info de time zone utc (+00):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">select now();
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># now
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">#-------------------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 2022-05-27 15:36:59.903336+00
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">#(1 row)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>E o mesmo com python:&lt;/p>
&lt;h4 id="python">python&lt;/h4>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">datetime&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">datetime&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">datetime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">astimezone&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">tzinfo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#datetime.timezone(datetime.timedelta(days=-1, seconds=75600), &amp;#39;-03&amp;#39;)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ou seja, o sistema no qual está rodando o python, está com o time zone -03 em relação ao UTC.&lt;/p>
&lt;blockquote>
&lt;p>⚠️ Atenção, dependendo de como estiver configurado seu sistema, esse resultado poderá ser diferente do meu.&lt;/p>
&lt;/blockquote>
&lt;h2 id="resumindo">Resumindo&lt;/h2>
&lt;ul>
&lt;li>É possível usar tanto o &lt;code>timedelta&lt;/code> como &lt;code>TimeZone&lt;/code>, do &lt;code>pytz&lt;/code>, para definir o &lt;code>tzinfo&lt;/code> de uma instância &lt;code>datetime&lt;/code>. Contudo, é preciso cuidado com relação ao método usado na atribuição do &lt;code>tzinfo&lt;/code>:
&lt;ul>
&lt;li>Caso se esteja usando uma instância &lt;code>TimeZone&lt;/code> do &lt;code>pytz&lt;/code>, &lt;strong>é indicado usar o método &lt;code>localize&lt;/code>&lt;/strong>;&lt;/li>
&lt;li>Já usando o &lt;code>timedelta&lt;/code>, pode-se fazê-lo tanto na criação da instância &lt;code>datetime&lt;/code>, quanto usando o método &lt;code>replace&lt;/code> do objeto &lt;code>datetime&lt;/code> já instanciado;&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">BR_TIME_ZONE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pytz&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">timezone&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;America/Sao_Paulo&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">date_time_ibject&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2022&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">27&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BR_TIME_ZONE&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">localize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">date_time_ibject&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># usando timedelta&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BR_TIME_ZONE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">timezone&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">timedelta&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hours&lt;/span>&lt;span class="o">=-&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">date_time_object&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2022&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">27&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tzinfo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">BR_TIME_ZONE&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># OU&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">date_time_object&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2022&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">27&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">date_time_object&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tzinfo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">BR_TIME_ZONE&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>
&lt;p>Os dados de data e hora são sempre armazenados em UTC no PostgreSQL, independente de estarmos ou não usando campos com time zone conscientes. Logo, ao persistir um dado consciente, o mesmo será convertido e persistido em UTC, mesmo em campos ingênuos. Objetos sem informação de time zone, serão persistido como tais por se entender já estarem em UTC. A diferença em relação a esses tipos de campos se dá pelo fato do primeiro armazenar a informação do time zone e o último, não.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Outra diferença entre campo consciente e ingênuo se dá nas consultas: Os campos conscientes, ao serem consultados por uma seção com time zone diferente do padrão (UTC), retornará os dados convertidos ao time zone da seção;&lt;/p>
&lt;ul>
&lt;li>Para definir o time zone de uma seção, pode-se usar:&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># psql
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">set timezone = &amp;#39;America/Sao_Paulo&amp;#39;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># ou
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># SQLAlchemy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">engine = create_engine(..., connect_args={&amp;#34;options&amp;#34;: &amp;#34;-c timezone=-3&amp;#34;})
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Não poderia deixar de agradecer ao &lt;a href="https://felipesbarros.github.io/post/aprendendo-sobre-datetime-sqlalchemy-e-postgresql-a-partir-de-bugs/" target="_blank" rel="noopener">@cuducos&lt;/a> pelo incentivo em resolver os problemas encontrados e ajuda na revisão do texto. Eu acabei encontrando essas soluções antes de ter tempo de seguir a sugestão dele: “tentar identificar o como o SQLAlchemy estava fazendo o &lt;code>insert&lt;/code> dos dados e o resgate dos mesmos”. Vejo que, de alguma forma, foi o direcionamento que acabei tomando para entender a diferença nos fuso horários retornados pelas consultas.&lt;/p></description></item><item><title>Criando um sistema para gestão de dados geográficos de forma simples e robusta III.</title><link>http://ambientalanalytics.com/post/geodjango3/</link><pubDate>Sat, 04 Jun 2022 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/post/geodjango3/</guid><description>&lt;p>Caso não tenha visto as publicações anteriores, deixo aqui o link e os temas abordados:&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://www.linkedin.com/pulse/criando-um-sistema-para-gest%C3%A3o-de-dados-geogr%C3%A1ficos-e-felipe-/" target="_blank" rel="noopener">Na primeira publicação&lt;/a> falo sobre o &lt;code>django-geojson&lt;/code> para simular um campo geográfico no models; o &lt;code>geojson&lt;/code> para criar um objeto da classe geojson e realizar as validações necessárias para garantir robustez do sistema, e a criação do formulário de registro de dados usando o &lt;code>ModelForm&lt;/code>;&lt;/li>
&lt;li>&lt;a href="https://www.linkedin.com/pulse/criando-um-sistema-para-gest%C3%A3o-de-dados-geogr%C3%A1ficos-e-felipe--1e/" target="_blank" rel="noopener">Na segunda publicação&lt;/a> apresento os validadores de campo do &lt;code>Django&lt;/code> como uma ferramenta fundamental na qualidade dos dados espaciais, &lt;strong>sem depender de infraestrutura SIG (GIS)&lt;/strong>.&lt;/li>
&lt;/ol>
&lt;p>Agora a ideia é implementar um webmap usando o módulo &lt;code>django-leaflet&lt;/code> para apresentar os fenômenos mapeados com algumas informações no popup do mapa. Para isso iremos:&lt;/p>
&lt;ol>
&lt;li>usar o &lt;code>GeoJSONLayerView&lt;/code>, do &lt;a href="https://django-geojson.readthedocs.io/en/latest/" target="_blank" rel="noopener">&lt;code>django-geojson&lt;/code>&lt;/a> para retornar os dados salvos no formato apropriado para exibição no webmap;&lt;/li>
&lt;li>usar o &lt;a href="https://django-leaflet.readthedocs.io/en/latest/" target="_blank" rel="noopener">&lt;code>django-leaflet&lt;/code>&lt;/a> para, além de implementar o webmap, podermos usar várias outras ferramentas (widget);&lt;/li>
&lt;/ol>
&lt;p>Vamos lá!&lt;/p>
&lt;h2 id="view-geojsonlayerview">View GeoJSONLayerView&lt;/h2>
&lt;p>A serialização ou, em inglês &lt;code>serialization&lt;/code>, é o processo/mecanismo de tradução dos objetos armazenados na base de dados em outros formatos (em geral, baseado em texto como, por exemplo, XML ou JSON), para serem enviados e/ou consumidos no processo de request/response.&lt;/p>
&lt;p>No nosso caso isso será importante, pois para apresentar os dados salvos em um webmap, precisaremos servi-los no formato &lt;code>geojson&lt;/code>. E é aí que o &lt;code>django-geojson&lt;/code> entra. Nós o utilizaremos para fazer a mágica acontecer ao usar a classe &lt;a href="https://django-geojson.readthedocs.io/en/latest/views.html#geojson-layer-view" target="_blank" rel="noopener">&lt;code>GeoJSONLayerView&lt;/code>&lt;/a>.&lt;/p>
&lt;p>A classe &lt;code>GeoJSONLayerView&lt;/code> é um &lt;a href="https://docs.djangoproject.com/en/3.2/topics/class-based-views/mixins/" target="_blank" rel="noopener">mixin&lt;/a> que, em base ao modelo informado do nosso projeto, serializa os dados transformando-os em &lt;code>geojson&lt;/code> e os servindo em uma &lt;code>view&lt;/code>. Acredite, é bastante coisa para apenas algumas linhas de código.&lt;/p>
&lt;p>Para entender a serialização, segue um exemplo…&lt;/p>
&lt;p>Ao acessar os dados do banco de dados do nosso projeto, temos uma &lt;code>QuerySet&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;gt;&amp;gt;&amp;gt; Fenomeno.objects.all()
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;QuerySet [&amp;lt;Fenomeno: fenomeno_teste&amp;gt;]&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ao acessar a geometria de um objeto do banco de dados do nosso projeto, temos um &lt;code>geojson&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;gt;&amp;gt;&amp;gt; Fenomeno.objects.get(pk=3).geom
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{&amp;#39;type&amp;#39;: &amp;#39;Point&amp;#39;, &amp;#39;coordinates&amp;#39;: [-42.0, -22.0]}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ao serializá-lo com o &lt;code>GeoJSONSerializer&lt;/code>, temos como retorno uma &lt;a href="https://datatracker.ietf.org/doc/html/rfc7946#section-3.3" target="_blank" rel="noopener">FeatureCollection&lt;/a> seguindo o formato &lt;code>geojson&lt;/code>, tendo como propriedades os campos do &lt;code>model&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;gt;&amp;gt;&amp;gt; from djgeojson.serializers import Serializer as GeoJSONSerializer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;gt;&amp;gt;&amp;gt; GeoJSONSerializer().serialize(Fenomeno.objects.all(), use_natural_keys=True, with_modelname=False)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;#39;{&amp;#39;crs&amp;#39;: {&amp;#39;properties&amp;#39;: {&amp;#39;href&amp;#39;: &amp;#39;http://spatialreference.org/ref/epsg/4326/&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;type&amp;#39;: &amp;#39;proj4&amp;#39;},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;type&amp;#39;: &amp;#39;link&amp;#39;},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;features&amp;#39;: [{&amp;#39;geometry&amp;#39;: {&amp;#39;coordinates&amp;#39;: [-42.0, -22.0], &amp;#39;type&amp;#39;: &amp;#39;Point&amp;#39;},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;id&amp;#39;: 3,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;properties&amp;#39;: {&amp;#39;data&amp;#39;: &amp;#39;2021-06-22&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;hora&amp;#39;: &amp;#39;02:07:57&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;nome&amp;#39;: &amp;#39;teste&amp;#39;},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;type&amp;#39;: &amp;#39;Feature&amp;#39;}],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;type&amp;#39;: &amp;#39;FeatureCollection&amp;#39;}&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mais sobre serialização pode ser encontrado &lt;a href="https://django-portuguese.readthedocs.io/en/1.0/topics/serialization.html" target="_blank" rel="noopener">aqui&lt;/a> ou &lt;a href="https://docs.djangoproject.com/en/3.2/ref/contrib/gis/serializers/" target="_blank" rel="noopener">aqui&lt;/a>, com outro exemplo relacionado a dado geográfico usando o &lt;code>GeoDjango&lt;/code>.&lt;/p>
&lt;p>Então, ciente de toda a mágica por trás do &lt;code>GeoJSONLayerView&lt;/code> e o seu resultado, vamos criar os testes para essa &lt;code>view&lt;/code>.&lt;/p>
&lt;h2 id="criando-os-testes-da-view">Criando os testes da &lt;code>view&lt;/code>&lt;/h2>
&lt;p>Como estou testando justamente uma &lt;code>view&lt;/code> que serializa o objeto do meu modelo em formato &lt;code>geojson&lt;/code>, precisarei desses dados salvos no banco de dados. Para tanto, vou adicionar ao &lt;code>setUp&lt;/code> do meu &lt;code>TestCase&lt;/code> valores válidos ao banco de dados do teste. Sem isso, não poderemos confirmar se a serialização está ocorrendo de forma correta. E, uma vez salvo, realizo um conjunto básico de testes:&lt;/p>
&lt;ul>
&lt;li>Confirmo se o status code do request (método “get”) ao path que pretendo usar para essa views (no caso, “/geojson/&amp;quot;), retorna 200, código que indica sucesso no processo de request/response. &lt;a href="https://en.wikipedia.org/wiki/List_of_HTTP_status_codes" target="_blank" rel="noopener">Veja mais sobre os códigos aqui&lt;/a>.&lt;/li>
&lt;li>Em seguida, confirmo se a resposta recebida é uma &lt;code>FeatureCollection&lt;/code> com os dados da instância criada anteriormente.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># testes.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">FenomenoGeoJsonTest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TestCase&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">setUp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Fenomeno&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">objects&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nome&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Teste&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;2020-01-01&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hora&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;09:12:12&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">geom&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Point&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;coordinates&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">22&lt;/span>&lt;span class="p">]},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">teste_geojson_status_code&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;geojson&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertEqual&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">status_code&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">teste_path_geojson_returns_valid_feature_collection&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;geojson&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertEqual&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;FeatureCollection&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;features&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Feature&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;popup_content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span&amp;gt;Nome: &amp;lt;/span&amp;gt;Teste&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;core.fenomeno&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;geometry&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Point&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;coordinates&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">42.0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mf">22.0&lt;/span>&lt;span class="p">]},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;crs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;EPSG:4326&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Obviamente, ambos testes falharão, pois, ainda não criamos a view e nem a designamos a um path do nosso sistema.&lt;/p>
&lt;p>Para fazê-los passar, vamos primeiro criar a view: Em &lt;code>views.py&lt;/code> criaremos uma classe nova, herdando da classe &lt;code>GeoJSONLayerView&lt;/code>. Ela será a view responsável por resgatar os dados e servir-nos como uma &lt;code>FeatureCollection&lt;/code> seguindo a estrutura de um &lt;code>geojson&lt;/code>.&lt;/p>
&lt;p>Um último detalhe é que, como estamos usando um &lt;code>Class Based-View&lt;/code>, ao final a convertemos em view, com o método &lt;code>as_view()&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># views.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">djgeojson.views&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">GeoJSONLayerView&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">map_proj.core.models&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Fenomeno&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">FenomenoGeoJson&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">GeoJSONLayerView&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Fenomeno&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">properties&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;popup_content&amp;#34;&lt;/span>&lt;span class="p">,)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">fenomeno_geojson&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FenomenoGeoJson&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">as_view&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="adicionando-propriedade-para-popup">Adicionando propriedade para popup&lt;/h2>
&lt;p>Percebam que no &lt;code>teste_geojson_FeatureCollection&lt;/code> eu já estou considerando que o &lt;code>geojson&lt;/code> virá com &lt;code>properties&lt;/code> com o nome de &lt;code>popup-content&lt;/code>. Essa &lt;code>property&lt;/code> ainda deverá ser criada no model em questão e poderá ter quantas informações acharmos pertinentes. Se tratam das informações do model a serem apresentadas no popup do mapa.&lt;/p>
&lt;p>Por agora estou apenas informando o nome do fenômeno mapeado mas, mais à frente, podemos incrementar, adicionando um &lt;code>get_absolute_url&lt;/code> por exemplo, para poder acessar aos detalhes do fenômeno diretamente a partir do popup do mapa.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#models.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nd">@property&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">popup_content&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">nome&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="adicionando-um-path-a-view">Adicionando um path a view&lt;/h2>
&lt;p>Para poder acessar essa view, precisamos incorporá-la na nossa &lt;code>urls.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># urls.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">django.contrib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">admin&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">django.urls&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">map_proj.core.views&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">fenomeno_geojson&lt;/span> &lt;span class="c1"># novo!&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">urlpatterns&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;admin/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">admin&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">site&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">urls&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;geojson/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">fenomeno_geojson&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;geojson&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="c1"># novo!&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Com isso teremos os nossos últimos testes passando. Se ainda assim você tiver curiosidade, pode executar o &lt;code>runserver&lt;/code> e acessar os dados pela url http://127.0.0.1:8000/geojson/. O resultado esperado são os dados servidos em &lt;code>geojson&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;type&amp;#34;: &amp;#34;FeatureCollection&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;features&amp;#34;: [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;type&amp;#34;: &amp;#34;Feature&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;properties&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;popup_content&amp;#34;: &amp;#34;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span&amp;gt;Nome: &amp;lt;/span&amp;gt;Teste&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;model&amp;#34;: &amp;#34;core.fenomeno&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;id&amp;#34;: 1,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;geometry&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;Point&amp;#34;, &amp;#34;coordinates&amp;#34;: [-42.0, -22.0]},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;crs&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;name&amp;#34;, &amp;#34;properties&amp;#34;: {&amp;#34;name&amp;#34;: &amp;#34;EPSG:4326&amp;#34;}},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>⚠️ Garanta que você já tenha inserido algum dado ao seu projeto ;)&lt;/p>
&lt;p>Pronto, já temos uma &lt;code>view&lt;/code> nos servindo os dados em formato &lt;code>geojson&lt;/code>. Vamos ao &lt;code>Django-leaflet&lt;/code>, para entender como montar um webmap.&lt;/p>
&lt;h2 id="django-leaflet">Django-leaflet&lt;/h2>
&lt;p>Para saber mais sobre o &lt;code>django-leaflet&lt;/code>, recomendo dar uma olhada na página &lt;a href="https://pypi.org/project/django-leaflet/" target="_blank" rel="noopener">pypi&lt;/a> e na &lt;a href="https://django-leaflet.readthedocs.io/en/latest/installation.html" target="_blank" rel="noopener">documentação&lt;/a>.&lt;/p>
&lt;p>Você deve estar se perguntando: “por quê usar o &lt;code>django-leaflet&lt;/code> se eu posso usar o &lt;code>leaflet&lt;/code> “puro”, já que se trata de uma biblioteca JavaScript para produção do mapa no frontend?”.&lt;/p>
&lt;p>Os autores do projeto &lt;code>django-leaflet&lt;/code> deixam alguns pontos que justificam sua adoção na página da documentação. Das quais eu destaco:&lt;/p>
&lt;ul>
&lt;li>Possibilidade de uso das ferramentas de edição de geometría usando os &lt;code>widget&lt;/code>;&lt;/li>
&lt;li>Fácil integração dos &lt;code>widgets&lt;/code> na página admin do Django;&lt;/li>
&lt;li>Controle da aparência dos mapas a partir do Django &lt;code>settings.py&lt;/code>;&lt;/li>
&lt;/ul>
&lt;p>⚠️ E por último, mas não menos importante:&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>django-leaflet&lt;/code> é compatível com os campos &lt;code>django-geojson&lt;/code>, o que permite o uso de dados geográficos sem a necessidade de uma base de dados espaciais. O motivo de toda essa série que tenho produzido :)&lt;/p>
&lt;/blockquote>
&lt;p>Bem legal! Eles criaram um pacote já compatível com o pacote &lt;code>django-geojson&lt;/code>, que nos permite simular campos geográficos sem a necessidade de toda a infraestrutura de uma base de dados de SIG (PostGIS, por exemplo).&lt;/p>
&lt;p>⚠️ Porém, atenção ao seguinte detalhe:+&lt;/p>
&lt;blockquote>
&lt;p>O &lt;code>django-leaflet&lt;/code> depende da biblioteca &lt;a href="https://pypi.org/project/GDAL/" target="_blank" rel="noopener">GDAL&lt;/a>, não se esqueça de instalá-la antes.&lt;/p>
&lt;/blockquote>
&lt;h2 id="instalando-django-leaflet">Instalando &lt;code>django-leaflet&lt;/code>&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">pip install django-leaflet
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Após a sua instalação é necessário incluí-lo no &lt;code>settings.py&lt;/code> como &lt;em>INSTALLED_APPS&lt;/em>.&lt;/p>
&lt;p>⚠️ Não esqueça de adicioná-lo ao &lt;code>requirements.txt&lt;/code> do projeto, também.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># settings.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">INSTALLED_APPS = [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;djgeojson&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;leaflet&amp;#39;, # novo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="usando-o-leaflet">Usando o leaflet&lt;/h3>
&lt;p>Com &lt;code>leaflet&lt;/code> instalado, devemos então:&lt;/p>
&lt;ol>
&lt;li>Na pasta da nossa app, vamos criar uma pasta chamada “templates”;&lt;/li>
&lt;li>E nessa pasta, criar um arquivo HTML (neste caso vou chamar de “map.html”;&lt;/li>
&lt;li>Nessa página vamos carregar as &lt;code>template_tags&lt;/code> do leaflet para poder usar &lt;code>leaflet_js&lt;/code>, &lt;code>leaflet_css&lt;/code> e o &lt;code>leaflet_map&lt;/code>:&lt;/li>
&lt;/ol>
&lt;p>Nosso &lt;code>map.html&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">% load leaflet_tags %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% leaflet_js %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% leaflet_css %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% leaflet_map &amp;#34;yourmap&amp;#34; %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Essas &lt;code>template_tags&lt;/code> irão tentar acessar as configurações do leaflet presentes no &lt;code>settings.py&lt;/code> da app, caso existam. Do contrário, serão usados valores padrão de configuração. O interessante dessas &lt;code>template_tags&lt;/code> é que com elas podemos customizar tais configurações a cada template;&lt;/p>
&lt;p>Como a ideia é apenas renderizar essa página, vou adicionar ao &lt;code>urls.py&lt;/code> um path a ela, usando o &lt;a href="https://docs.djangoproject.com/en/4.0/topics/class-based-views/#basic-examples" target="_blank" rel="noopener">&lt;code>TemplateView&lt;/code>&lt;/a>. Com isso, ao receber um request neste path, a responsta será direcionada à renderização dessa página:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">#urls.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">from django.contrib import admin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">from django.urls import path
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">from django.views.generic import TemplateView
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">from map_proj.core.views import fenomeno_geojson
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">urlpatterns = [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> path(&amp;#34;admin/&amp;#34;, admin.site.urls),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> path(&amp;#34;geojson/&amp;#34;, fenomeno_geojson, name=&amp;#34;geojson&amp;#34;),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> path(&amp;#34;map/&amp;#34;, TemplateView.as_view(template_name=&amp;#34;map.html&amp;#34;), name=&amp;#34;map&amp;#34;),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Isso já o suficiente para termos nosso webmap apresentado:&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="" srcset="
/post/geodjango3/leaflet_1_hu9a26a11b7d206da89c073eea76e1ee3e_45208_fb0d8ce3031810a2ad69a97d050d1d75.webp 400w,
/post/geodjango3/leaflet_1_hu9a26a11b7d206da89c073eea76e1ee3e_45208_f31cb46d97130be4e463ed3ee96cb775.webp 760w,
/post/geodjango3/leaflet_1_hu9a26a11b7d206da89c073eea76e1ee3e_45208_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="http://ambientalanalytics.com/post/geodjango3/leaflet_1_hu9a26a11b7d206da89c073eea76e1ee3e_45208_fb0d8ce3031810a2ad69a97d050d1d75.webp"
width="760"
height="385"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>Imagino que não seja o que esperava, né? Fique calmo. O leaflet buscou as configurações do mapa e, como não encontrou, retornou o mesmo com as configurações padrão. Veremos em breve como alterar as configurações do mapa.&lt;/p>
&lt;p>Antes disso, vamos “linkar” a view que nos serve o &lt;code>geojson&lt;/code> com os dados salvos no banco com o webmap em questão, para que os dados sejam apresentados.&lt;/p>
&lt;h2 id="renderizando-o-geojson-no-mapa">Renderizando o &lt;code>geojson&lt;/code> no mapa&lt;/h2>
&lt;p>Lembra que temos uma view que serializa os dados armazenados no banco e nos serve como uma &lt;code>FeatureCollection&lt;/code> e que podemos acessar tais dados pelo path geojson/?&lt;/p>
&lt;p>Então, iremos adicionar um script à nossa página no qual uma variável &lt;code>dataurl&lt;/code> receberá os dados dessa url adicionando tais dados ao mapa, assim que o mesmo for inicializado, desencadeando o processo de construção da popup de cada feição apresentada com sua posterior inserção ao mapa:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">{% load leaflet_tags %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">dataurl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;{% url &amp;#34;geojson&amp;#34; %}&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;map:init&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">map&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">detail&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Download GeoJSON data with Ajax
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">fetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataurl&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">then&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">resp&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">resp&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">then&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">L&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">geoJson&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">onEachFeature&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">onEachFeature&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">feature&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">layer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">props&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span&amp;gt;Nome: &amp;lt;/span&amp;gt; &amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">feature&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">properties&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">popup_content&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">layer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bindPopup&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">props&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}}).&lt;/span>&lt;span class="nx">addTo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% leaflet_js %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% leaflet_css %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{% leaflet_map &amp;#34;yourmap&amp;#34; %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Veja que, para a criação da variável &lt;code>dataurl&lt;/code>, estamos usando a &lt;code>template_tag&lt;/code> do django: &lt;code>var dataurl = '{% url &amp;quot;geojson&amp;quot; %}'&lt;/code>;&lt;/p>
&lt;p>Veja mais sobre ela &lt;a href="https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#url" target="_blank" rel="noopener">aqui&lt;/a>.&lt;/p>
&lt;p>Repare também que, neste processo, a cada &lt;code>Feature&lt;/code>, será carregada as suas propriedades a serem apresentadas no popup:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">L.geoJson(data, {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> onEachFeature: function onEachFeature(feature, layer) {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> var props = &amp;#34;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span&amp;gt;Nome: &amp;lt;/span&amp;gt; &amp;#34; + feature.properties.popup_content + &amp;#34;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&amp;#34;;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> layer.bindPopup(props);
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }}).addTo(map);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Com o &lt;code>runserver&lt;/code> em execução, já poderemos ver o nosso mapa com o dado carregado e as propriedades que definimos no popup:&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="" srcset="
/post/geodjango3/leaflet_2_hucd19f92744567b6fda98afa7178b5110_73526_b355c028858ad5555ea335914c850e75.webp 400w,
/post/geodjango3/leaflet_2_hucd19f92744567b6fda98afa7178b5110_73526_5cc96794042126ee832c3f602d1f5371.webp 760w,
/post/geodjango3/leaflet_2_hucd19f92744567b6fda98afa7178b5110_73526_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="http://ambientalanalytics.com/post/geodjango3/leaflet_2_hucd19f92744567b6fda98afa7178b5110_73526_b355c028858ad5555ea335914c850e75.webp"
width="760"
height="428"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
)&lt;/p>
&lt;p>⚠️ Garanta que você já tenha inserido algum dado ao seu projeto ;)&lt;/p>
&lt;h3 id="mudando-o-tamanho-do-webmap">Mudando o tamanho do webmap:&lt;/h3>
&lt;p>Antes de passarmos às configurações do leaflet, podemos alterar as dimensões do mapa definindo um style. Por exemplo, para que o mapa ocupe toda a área possível do navegador, basta adicionarmos:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;lt;style&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> #yourmap {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> width: 100%;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> height: 100%;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;/style&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="" srcset="
/post/geodjango3/leaflet_3_hu594f8773687226c609037d5d7db1948a_118216_79b024d56fd9e770d70351c66f5834da.webp 400w,
/post/geodjango3/leaflet_3_hu594f8773687226c609037d5d7db1948a_118216_ffcf6dd7b01a31aff19c64b7bb559392.webp 760w,
/post/geodjango3/leaflet_3_hu594f8773687226c609037d5d7db1948a_118216_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="http://ambientalanalytics.com/post/geodjango3/leaflet_3_hu594f8773687226c609037d5d7db1948a_118216_79b024d56fd9e770d70351c66f5834da.webp"
width="760"
height="428"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
)&lt;/p>
&lt;h2 id="configurações-do-leaflet">Configurações do leaflet&lt;/h2>
&lt;p>Bom, além das &lt;code>template_tags&lt;/code> do leaflet, o uso do &lt;code>django-leaflet&lt;/code> nos permite definirmos as suas configurações no &lt;code>settings.py&lt;/code> da app, a partir da seção &lt;code>LEAFLET_CONFIG&lt;/code>.&lt;/p>
&lt;p>Dentre as &lt;a href="https://django-leaflet.readthedocs.io/en/latest/templates.html#configuration" target="_blank" rel="noopener">configurações possíveis&lt;/a>, vou usar apenas o par de coordenadas ao qual o mapa deverá estar centralizado por padrão (&lt;code>DEFAULT_CENTER&lt;/code>) e o zoom padrão (&lt;code>DEFAULT_ZOOM&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">#settings.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">LEAFLET_CONFIG = {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;DEFAULT_CENTER&amp;#39;: (-22, -42),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;DEFAULT_ZOOM&amp;#39;: 7,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Com isso nosso mapa sempre será apresentado centralizado nas coordenadas (-22, -42) e com o zoom 7:&lt;/p>
&lt;p>Pronto: com esses três artigos, já temos um sistema com formulário de inserção de dados, com as devidas validações dos dados preenchidos no mesmo, assim como um webmap apresentando-os ao mundo :-).&lt;/p>
&lt;p>&lt;del>Na próxima publicação vamos ver como fazer o deploy desse sistema no heroku 🚀.&lt;/del> Infelizmente não darei sequencia a essa publicação já que o Heroku não tem mais instâncias gratuitas :/&lt;/p></description></item><item><title>Bug buster</title><link>http://ambientalanalytics.com/post/bugbuster/</link><pubDate>Fri, 15 Apr 2022 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/post/bugbuster/</guid><description>&lt;h2 id="como-tudo-começou">Como tudo começou:&lt;/h2>
&lt;p>Estou trabalhando num projeto onde uma das funções do python é executada e recebe um um parâmtero pelo terminal. Um detalhe é que esse parâmetro é o nome de uma pessoa. Um ponto que não previ no processo de desenvolvimento é que nomes, como qualquer outro elemento textual da lingua portuguesa, podem ter acentos (ou “caracteres especiais”).&lt;/p>
&lt;p>Pois é, foi praticamente sem querer que vi, olhando os logs produzidos, que os nomes com acento estavam com problema de &lt;code>encoding&lt;/code>.&lt;/p>
&lt;p>E assim começou a minha caça ao bug. Uma caça que me tomou um dia e meio. Mas foi de grande aprendizado.&lt;/p>
&lt;h3 id="um-pouco-do-contexto">Um pouco do contexto:&lt;/h3>
&lt;p>Antes de descrever essa aventura, comento um pouco o fluxo do programa que apresentou erro:&lt;/p>
&lt;ol>
&lt;li>Uma função é executada pelo terminal e recebe um parâmtero, que é o nome de uma pessoa;&lt;/li>
&lt;li>Esse nome é usado para instanciar um objeto. Logo no &lt;code>__init__&lt;/code> tenho o ponto de acesso ao que foi informado pelo terminal com a incorporação do mesmo como atributo da instância.&lt;/li>
&lt;li>Alguns processamnetos, que não vem ao caso, são realizados;&lt;/li>
&lt;li>O log do processamento realizado é persistirdo numa base de dados usando o &lt;code>SQLAlchemy&lt;/code>, onde a tabela que o recebe possui um campo id e outro json, com os logs organizados em tal formato.&lt;/li>
&lt;/ol>
&lt;h2 id="e-agora-por-onde-começar">E agora? Por onde começar?&lt;/h2>
&lt;p>###Buscando o bug no terminal&lt;/p>
&lt;p>Como o parâmetro estava sendo passado por terminal, achei que o problema estava nesse ponto: no terminal. Primeiro passo: checar o encoding usado pelo sistema. Mas logo vi que estava tudo em &lt;code>utf-8&lt;/code>, o que por sí, não deveria apresentar problema.&lt;/p>
&lt;p>Como o nome estava sendo passado como um parâmetro do sistema a partir de uma variável, aproveitei para checar se o erro não estava aí. Nada que um print e alguns testes no terminal não resolva. E nada, os nomes armazenados na variáve e passados como parâmetro não sofriam qualquer alteração neste processo inicial.&lt;/p>
&lt;h3 id="interseção-terminalpython">Interseção terminal/python&lt;/h3>
&lt;p>Achei, então que o erro estava em alguma incompatibilidade entre o que era passado no terminal e o como o python estava recebendo.&lt;/p>
&lt;p>Segundo passo, então, foi checar o ponto de contato entre terminal e o python. Ler o seguinte trecho, de uma resposta do stackOverFlow me deu a certeza de que era aí o erro:&lt;/p>
&lt;blockquote>
&lt;p>When Python does not detect that it is printing to a terminal, sys.stdout.encoding is set to None.&lt;/p>
&lt;/blockquote>
&lt;p>Ou seja, quando o python não pode detectar o que está sendo apresentado ao terminal o &lt;code>sys.stdout.encoding&lt;/code> é definido como &lt;code>None&lt;/code>; Ora, estou passando um parametro a partir de uma variável do terminal, logo um string. O Python não consegue identificar o encoding dessa string e está definindo, então o encoding a None, o que deve estar gerando o erro.&lt;/p>
&lt;p>Tentando resolver isso, busquei alguma forma de declarar o encoding… Cheguei a adicionar ao &lt;code>__init__&lt;/code>, quando a classe é instanciada e recebe o nome da pessoa os métodos, &lt;code>.encode().decode('utf-8')&lt;/code> ao objeto que recebe o valor. Parecia que ia funcionar, vejam:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">nome&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;Felipe Sodré&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sa">b&lt;/span>&lt;span class="s1">&amp;#39;Felipe Sodr&lt;/span>&lt;span class="se">\xc3\xa9&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># e ao adicionar o decode o texto volta ao normal...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">nome&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">encode&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">decode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;utf-8&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;Felipe Sodré&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“Meio” gambiarra, não? Mas o importante é se funcionar.&lt;/p>
&lt;p>Contudo, o que parecia a solução, foi logo por agua abaixo na primeira rodada de teste. O nome continua com erro de encoding.&lt;/p>
&lt;p>Decidí, então, usar o módulo &lt;code>logging&lt;/code> para apresentar o nome recebido pelo terminal e nome após a classe estar instanciada durante o processamento. Aliás, o &lt;a href="https://twitter.com/dunossauro" target="_blank" rel="noopener">@dunosauro&lt;/a> apresentou uma &lt;a href="https://www.youtube.com/watch?v=PGAOqAWuwC0" target="_blank" rel="noopener">live muito boa sobre o uso do &lt;code>logging&lt;/code>&lt;/a>…&lt;/p>
&lt;p>Bom, ao usar o &lt;code>logging&lt;/code> tive certeza de que estava tentando resolver o erro no ponto errado, todas as mensagens de log estavam sem o tal erro de encoding, mas no log persistido no banco de dados seguia com o maldito erro…&lt;/p>
&lt;p>Será que o banco de dados está configurado com uma encoding diferente?&lt;/p>
&lt;h3 id="no-banco-de-dados">No banco de dados…&lt;/h3>
&lt;p>✔️ Banco configurado como ‘utf-8’;&lt;/p>
&lt;p>Até que me veio uma luz: nas mensagens de log o nome está sem erro. Mas o log que está sendo persistido no banco de dados ( que são algumas dessas mensagens filtradas para monitorar alguns pontos importantes do sistema) é uma compilação salva em um campo JSON.&lt;/p>
&lt;p>“Bom deve ser nesse ponto, então.”, pensei.&lt;/p>
&lt;h3 id="reproduzindo-o-erro-em-json">Reproduzindo o erro em &lt;code>JSON&lt;/code>&lt;/h3>
&lt;p>Parti então para tentar reproduzir esse erro:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&lt;/span>&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&lt;/span>&lt;span class="n">info&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;nome&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s1">&amp;#39;Felipe Sodré&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;idade&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">38&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&lt;/span>&lt;span class="n">info&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;nome&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;Felipe Sodré&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;idade&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">28&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;{&amp;#34;nome&amp;#34;: &amp;#34;Felipe Sodr&lt;/span>&lt;span class="se">\xc3\xa9&lt;/span>&lt;span class="s1">&amp;#34;, &amp;#34;idade&amp;#34;: 38}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Pronto! Aí está o problema. No processo de conversão do dicionário ao JSON, há algum tipo de conversão que gera o erro de encoding.&lt;/p>
&lt;p>Não levei “muito tempo” (tempo é relativo, né?) para encontrar que o método &lt;a href="https://docs.python.org/3/library/json.html#json.dumps" target="_blank" rel="noopener">&lt;code>dumps()&lt;/code>&lt;/a> possui o parâmetro &lt;code>ensure_ascii&lt;/code>, com valor padrão &lt;code>True&lt;/code>, que garante que as &lt;code>strings&lt;/code> do JSON que possuam caracteres não-ASCII estejam com scape.:&lt;/p>
&lt;blockquote>
&lt;p>If ensure_ascii is true (the default), the output is guaranteed to have all incoming non-ASCII characters escaped. If ensure_ascii is false, these characters will be output as-is.&lt;/p>
&lt;/blockquote>
&lt;p>Testei usando o dumps() com ensur_ascii=False:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ensure_ascii&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;{&amp;#34;nome&amp;#34;: &amp;#34;Flávia Duarte Nascimento&amp;#34;, &amp;#34;idade&amp;#34;: 12}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Pronto, ponto de erro encontrado. Basta adicionar o parâmetro apra False e tudo se resolveria.&lt;/p>
&lt;p>Mas não foi bem assim, ainda faltava um ponto. Eu não estava gerando o dump e salvando no banco. O que estou fazendo é passar o dado, ainda em dicionário, para o banco usando o SQLAlchemy e ele cuida disso para mim.&lt;/p>
&lt;p>Como, ou melhor, onde, então, eu devo informar esse &lt;code>ensure_ascii&lt;/code>?&lt;/p>
&lt;h2 id="enfim-a-solução">Enfim, a solução:&lt;/h2>
&lt;p>Foi lendo &lt;a href="https://stackoverflow.com/a/36438671" target="_blank" rel="noopener">essa responta no SOF&lt;/a> que entendí que como o ORM SQLAlchemy está cuidadno disso para mim, ele possui um serializador e que o mesmo é, nada mais, nada menos que os métodos &lt;code>json.dumps()&lt;/code> e &lt;code>json.loads()&lt;/code>, passados na função &lt;code>create_engine&lt;/code> como um kwargs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">engine&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">create_engine&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">json_serializer&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>O golpe final foi ao ler a docuementação do SQLAlchemy sobre &lt;a href="https://docs.sqlalchemy.org/en/14/core/type_basics.html#sqlalchemy.types.JSON" target="_blank" rel="noopener">o tipo de dado JSON&lt;/a> e aprender que podemos customizar o serializador. Olha só o exemplo da documentação, me dando de “bandeja” a solução para o bug em questão:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">engine&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">create_engine&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;sqlite://&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json_serializer&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">lambda&lt;/span> &lt;span class="n">obj&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">obj&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ensure_ascii&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Agora sim, vida que segue, graças à persistẽncia e perseverança na caça aos bugs.&lt;/p>
&lt;p>Ah, claro. Essa investigação contou com a ajuda de outros colegas que dedicaram alguns minutos para conversar e propor soluções, tabém. Muito obrigado!&lt;/p>
&lt;h2 id="atualização">Atualização&lt;/h2>
&lt;p>Quando achei que estava tudo funcionando e coloquei em produção a correção, eis que me deparo com um novo erro. Dessa vez um &lt;code>TypeError&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">raise&lt;/span> &lt;span class="ne">TypeError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s1">&amp;#39;Object of type &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="vm">__class__&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="vm">__name__&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1"> &amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sqlalchemy&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">exc&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">StatementError&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">builtins&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">TypeError&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">Object&lt;/span> &lt;span class="n">of&lt;/span> &lt;span class="nb">type&lt;/span> &lt;span class="n">datetime&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">JSON&lt;/span> &lt;span class="n">serializable&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Com algumas pesquisas, pude identificar que, como estou indicando um serializador, Todo objeto a ser inluido no JSON passará por ele. Mas, como o próprio erro informa, um objeto &lt;code>datetime&lt;/code> não pode ser seriaizado. E por isso que o método &lt;code>json.dumps()&lt;/code>, além de ter o parâmetro &lt;code>ensure_ascii&lt;/code>, possui um argumento para a serialização padrão.&lt;/p>
&lt;p>Portanto o último bug foi resolvido usando o parâmetro &lt;code>default=str&lt;/code>. Ou seja, o serializador padrãoa pe transformar o objeto a uma classe string.&lt;/p>
&lt;p>O codigo ficou, então da seguinte forma:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">engine = create_engine(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;sqlite://&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> json_serializer=lambda obj: json.dumps(obj, ensure_ascii=False, default=str))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>E vocês, que estratégias adotam na caça aos bugs?&lt;/p>
&lt;p>Note: image from &lt;a href="https://www.mclibre.org/consultar/documentacion/listados/thepracticaldev.html" target="_blank" rel="noopener">@ThePracticalDev&lt;/a>&lt;/p></description></item><item><title>Criando um sistema para gestão de dados geográficos de forma simples e robusta II.</title><link>http://ambientalanalytics.com/post/geodjango1/</link><pubDate>Sun, 11 Jul 2021 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/post/geodjango1/</guid><description>&lt;p>&lt;em>Artigo publicado também no &lt;a href="https://www.linkedin.com/pulse/criando-um-sistema-para-gest%25C3%25A3o-de-dados-geogr%25C3%25A1ficos-e-felipe--1e" target="_blank" rel="noopener">linkedin&lt;/a>.&lt;/em>&lt;/p>
&lt;p>Na &lt;a href="https://www.linkedin.com/pulse/criando-um-sistema-para-gest%C3%A3o-de-dados-geogr%C3%A1ficos-e-felipe-/" target="_blank" rel="noopener">primeira publicação&lt;/a> onde exploro a possibilidade de implementar um sistema de gestão de dados geoespaciais com Django, sem a necessidade de usar um servidor com PostGIS, vimos sobre:&lt;/p>
&lt;ul>
&lt;li>o &lt;code>django-geojson&lt;/code> para simular um campo geográfico no models;&lt;/li>
&lt;li>o &lt;code>geojson&lt;/code> para criar um objeto da classe geojson e realizar as validações necessárias para garantir robustez do sistema;&lt;/li>
&lt;li>a criação do formulário de registro de dados usando o &lt;code>ModelForm&lt;/code>;&lt;/li>
&lt;/ul>
&lt;p>Agora é hora de evoluir e expandir um pouco o sistema criado. Nessa publicação vamos criar validadores de longitude e latitude para poder restringir a inserção de dados a uma determinada região. Com isso, o próximo passo (e artigo) será criar o webmap no nosso sistema. Mas isso fica para breve.&lt;/p>
&lt;p>Vamos ao que interessa:&lt;/p>
&lt;h2 id="criando-validadores-de-longitude-e-latitude">Criando validadores de longitude e latitude&lt;/h2>
&lt;h3 id="sobre-os-validadores">Sobre os validadores:&lt;/h3>
&lt;p>Os validadores (&lt;a href="https://docs.djangoproject.com/en/3.2/ref/forms/validation/#validators" target="_blank" rel="noopener">&lt;code>validators&lt;/code>&lt;/a>, em inglês) fazem parte do sistema de validação de formulários e de campos do Django. Ao criarmos campos de uma determinada classe no nosso modelo, como por exemplo integer, o Django cuidará automaticamente da validação do valor passado a este campo pelo formulário, retornando um erro quando o usuário ingressar um valor de texto no campo em questão, por exemplo. O interessante é que além dos validadores já implementados para cada classe, podemos criar outros, conforme a nossa necessidade.&lt;/p>
&lt;blockquote>
&lt;p>Por que necessitamos um validador para os campos de &lt;code>latitude&lt;/code> e &lt;code>longitude&lt;/code>?&lt;/p>
&lt;/blockquote>
&lt;p>Como estou explorando o desenvolvimento de um sistema de gestão de dados geográficos com recursos limitados, ou seja, sem uma infraestrutura de operações e consultas espaciais, não poderei consultar se o par de coordenadas inserido pelo usuário está contido nos limites de um determinado estado (uma operação clássica com dados geográficos). Não ter essa possibilidade de validação poderá colocar em risco a qualidade do dado inserido.&lt;/p>
&lt;p>E como não se abre mão quando a questão é qualidade, uma saída será a criação de validadores personalizados para os campos de &lt;code>latitude&lt;/code> e &lt;code>longitude&lt;/code>, garantindo que esses possuem valores condizentes à nossa área de interesse.&lt;/p>
&lt;p>&lt;strong>O que precisamos saber&lt;/strong>: os validators são funções que recebem um valor, apenas (neste caso, o valor inserido pelo usuário no campo a ser validado), que passará por uma lógica de validação retornando um &lt;code>ValidationError&lt;/code> quando o valor inserido não passar na validação. Com o &lt;code>ValidationError&lt;/code> podemos customizar uma mensagem de erro, indicando ao usuário o motivo do valor não ter sido considerado válido, para que o mesmo corrija.&lt;/p>
&lt;p>Então, criarei validadores dos campos de &lt;code>latitude&lt;/code> e &lt;code>longitude&lt;/code> para sempre que entrarem com valores que não contemplem a área do estado do Rio de Janeiro, um &lt;code>ValidationError&lt;/code> será retornado.&lt;/p>
&lt;blockquote>
&lt;p>⚠️ Essa não é uma solução ótima já que, dessa forma, estamos considerando o bounding box do estado em questão, e com isso haverá áreas onde as coordenadas serão válidas, ainda que não estejam internas ao território estadual. Ainda assim, acredito que seja uma solução boa suficiente para alguns casos, principalmente por não depender de toda infraestrutura de GIS.&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>O que é um &lt;em>bounding box&lt;/em>?&lt;/strong>&lt;/p>
&lt;p>&lt;em>Bounding box&lt;/em> poderia ser traduzido por “retângulo envolvente” do estado, ou de uma feição espacial. Na imagem abaixo, vemos o território do estado do Rio de Janeiro e o retângulo envolvente que limita as suas coordenadas máximas e mínimas de longitude e latitude.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img src="RJ_bbox.png" alt="" loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>Percebam que, como mencionado antes, o que conseguimos garantir é que os pares de coordenadas estejam em alguma área interna ao retângulo em questão o que não garante que as mesmas estejam no território do estado do Rio de Janeiro.&lt;/p>
&lt;p>Por uma questão de organização, criei no &lt;code>settings.py&lt;/code> do meu projeto as variáveis com os valores máximos e mínimos de latitude e longitude. Essa proposta surgiu do cuducos, e achei que valia a pena implementar. Entendo que é mais organizado e evita possíveis falhas humanas, caso os mesmos valores tenham que ser usados em outras partes do sistema.&lt;/p>
&lt;p>Ao fim do meu &lt;code>settings.py&lt;/code>, adicionei:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># settings.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BOUNDING_BOX_LAT_MAX&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mf">20.764962&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BOUNDING_BOX_LAT_MIN&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mf">23.366868&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BOUNDING_BOX_LON_MAX&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mf">40.95975&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">BOUNDING_BOX_LON_MIN&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mf">44.887212&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Agora, sim. Vamos criar os testes:&lt;/p>
&lt;blockquote>
&lt;p>se você não entendeu o motivo pelo qual eu começo criando testes, dá uma olhada &lt;a href="https://www.linkedin.com/pulse/criando-um-sistema-para-gest%C3%A3o-de-dados-geogr%C3%A1ficos-e-felipe-/" target="_blank" rel="noopener">na primeira publicação&lt;/a>. Nela comento um pouco sobre a abordagem Test Driven Development (TDD).&lt;/p>
&lt;/blockquote>
&lt;h2 id="criando-os-testes">Criando os testes:&lt;/h2>
&lt;p>No &lt;code>tests.py&lt;/code>, criei uma nova classe de teste &lt;code>TestCase&lt;/code>, com o objetivo de testar os validadores simulando o uso do &lt;code>FenomenoForm&lt;/code>. Por isso criei staticmethod chamado &lt;code>create_form&lt;/code> que cria um dicionário com chaves e valores válidos do formulário em questão, que ao receber um conjunto de argumentos nomeados &lt;code>**kwargs&lt;/code> terá tais argumentos atualizados e usados para instanciar e retornar o &lt;code>FenomenoForm&lt;/code>.&lt;/p>
&lt;p>Fiz isso para, a cada teste, ter uma instância do &lt;code>FenoenoForm&lt;/code> alterando apenas os campos que quero simular valores a serem validados, sem ter que passar sempre todos os valores do &lt;code>ModelForm&lt;/code>. Assim, eu posso criar diferentes métodos de Test Case, usando o método criado anteriormente alterando o valor inicial a um inválido, testando se de fato um &lt;code>ValidationError&lt;/code> é retornado.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># tests.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">FenomenoFormValidatorsTest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TestCase&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nd">@staticmethod&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">create_form&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">**&lt;/span>&lt;span class="n">kwargs&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">valid_form&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;nome&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;Teste&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;2020-01-01&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;hora&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;09:12:12&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;longitude&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;latitude&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">21&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">valid_form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">**&lt;/span>&lt;span class="n">kwargs&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">form&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FenomenoForm&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">valid_form&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">form&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Nos métodos de teste uso primeiro o &lt;code>assertFalse&lt;/code> do método de validação do formulário (&lt;code>form.is_valid()&lt;/code>) para confirmar que o mesmo não é valido para, em seguida, testar com o &lt;code>assertEqual&lt;/code> se o texto da mensagem de erro é o que esperamos.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># tests.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_max_longitude_raises_error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">form&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create_form&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">longitude&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;-45&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertFalse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">is_valid&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertEqual&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">errors&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;longitude&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s1">&amp;#39;Coordenada longitude fora do contexto do estado do Rio de Janeiro&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_min_longitude_raises_error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">form&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create_form&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">longitude&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;-40&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertFalse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">is_valid&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertEqual&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">errors&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;longitude&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s1">&amp;#39;Coordenada longitude fora do contexto do estado do Rio de Janeiro&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_max_latitude_raises_error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">form&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create_form&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">latitude&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;-24&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertFalse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">is_valid&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertEqual&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">errors&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;latitude&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s1">&amp;#39;Coordenada latitude fora do contexto do estado do Rio de Janeiro&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_min_latitude_raises_error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">form&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create_form&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">latitude&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;-19&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertFalse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">is_valid&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertEqual&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">errors&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;latitude&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s1">&amp;#39;Coordenada latitude fora do contexto do estado do Rio de Janeiro&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Fazemos rodar os testes e teremos erros como esses:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Creating test database for alias &amp;#39;default&amp;#39;...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">System check identified no issues (0 silenced).
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...E.E..
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">======================================================================
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ERROR: test_max_latitude (map_proj.core.tests.FenomenoFormValidatorsTest)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">----------------------------------------------------------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Traceback (most recent call last):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> File &amp;#34;/media/felipe/DATA/Repos/Django_Leaflet_Test/map_proj/core/tests.py&amp;#34;, line 78, in test_max_latitude
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> self.assertEqual(form.errors[&amp;#34;latitude&amp;#34;][0], &amp;#39;Coordenada latitude fora do contexto do estado do Rio de Janeiro&amp;#39;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">KeyError: &amp;#39;latitude&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">======================================================================
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ERROR: test_min_latitude (map_proj.core.tests.FenomenoFormValidatorsTest)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">----------------------------------------------------------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Traceback (most recent call last):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> File &amp;#34;/media/felipe/DATA/Repos/Django_Leaflet_Test/map_proj/core/tests.py&amp;#34;, line 83, in test_min_latitude
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> self.assertEqual(form.errors[&amp;#34;latitude&amp;#34;][0], &amp;#39;Coordenada latitude fora do contexto do estado do Rio de Janeiro&amp;#39;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">KeyError: &amp;#39;latitude&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">----------------------------------------------------------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Ran 8 tests in 0.012s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">FAILED (errors=2)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Destroying test database for alias &amp;#39;default&amp;#39;...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ou seja, o &lt;code>forms&lt;/code> após ser validado deveria conter um atributo errors tendo como chave o nome do campo que apresentou dados inválidos. Como não temos os validadores criados, nenhum erro de validação foi acusado no campo de &lt;code>latitude&lt;/code>.&lt;/p>
&lt;h2 id="criando-e-usando-validadores">Criando e usando validadores:&lt;/h2>
&lt;p>Para superá-los criamos, enfim, os validadores em um arquivo &lt;code>validators.py&lt;/code>. Percebam que é nesse ponto que usarei os valores máximos e mínimos de latitude e longitude adicionados no &lt;code>settings.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># validators.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">django.core.exceptions&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">ValidationError&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">django.conf&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">settings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">validate_longitude&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">lon&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">lon&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">settings&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">BOUNDING_BOX_LON_MIN&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">lon&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">settings&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">BOUNDING_BOX_LON_MAX&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="n">ValidationError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Coordenada longitude fora do contexto do estado do Rio de Janeiro&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;erro longitude&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">validate_latitude&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">lat&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">lat&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">settings&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">BOUNDING_BOX_LAT_MIN&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">lat&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">settings&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">BOUNDING_BOX_LAT_MAX&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="n">ValidationError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Coordenada latitude fora do contexto do estado do Rio de Janeiro&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;erro latitude&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Com esses validadores estou garantindo que ambos latitude e longitude estejam na área de interesse e, caso contrário, retorno um erro informando ao usuário.&lt;/p>
&lt;p>E é preciso adicioná-los ao &lt;code>forms.py&lt;/code> para que sejam usados:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># forms.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">map_proj.core.validators&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">validate_longitude&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">validate_latitude&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">FenomenoForm&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ModelForm&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">longitude&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FloatField&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">validators&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">validate_longitude&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">latitude&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FloatField&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">validators&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">validate_latitude&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>No desenvolvimento dessa solução percebi pelos testes criados que, ao informar uma latitude ou longitude que não passe pela validação, a criação do campo &lt;code>geom&lt;/code> se tornava inválido por não receber um desses valores, gerando dois erros: o de validação do campo e o de validação do campo &lt;code>geom&lt;/code>. Lembre-se que é no método &lt;code>clean&lt;/code> do formulário que o campo &lt;code>geom&lt;/code> recebe os valores de &lt;code>longitude&lt;/code> e &lt;code>latitude&lt;/code> formando uma classe &lt;code>geojson&lt;/code> para, logo em seguida ser validado.&lt;/p>
&lt;p>Para evitar isso, alterei o método &lt;code>clean&lt;/code> de forma garantir que o campo &lt;code>geom&lt;/code> só seja criado e validado, quando ambos valores (&lt;code>longitude&lt;/code> e &lt;code>latitude&lt;/code>) existirem. Ou seja, tenham passado pelos validadores sem erro.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#forms.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">clean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cleaned_data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">super&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">clean&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lon&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">cleaned_data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;longitude&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lat&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">cleaned_data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;latitude&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="nb">all&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">lon&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">lat&lt;/span>&lt;span class="p">)):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="n">ValidationError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Erro em latitude ou longitude&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cleaned_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;geom&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Point&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">lon&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">lat&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">cleaned_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;geom&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">is_valid&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="n">ValidationError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Geometria inválida&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">cleaned_data&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>Outro ponto (na verdade, erro) importante que só percebi a partir dos testes é que no &lt;code>forms.py&lt;/code> eu não estava considerando o campo &lt;code>geom&lt;/code> na lista de &lt;code>fields&lt;/code> a serem usados. Com isso o mesmo não é passado ao banco de dados, mesmo passando pelo método &lt;code>clean&lt;/code> que o cria.&lt;/p>
&lt;/blockquote>
&lt;p>Por esse motivo, tive que alterar algumas coisas no &lt;code>forms.py&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>Inseri o campo &lt;code>geom&lt;/code> à tupla de &lt;code>fields&lt;/code> do &lt;code>forms.py&lt;/code>.&lt;/li>
&lt;li>Inseri o campo &lt;code>geom&lt;/code> com um widget de &lt;code>HiddenInput&lt;/code>. Esse último, o fiz por se tratar de um campo que não quero expor ao usuário, já que será criado automaticamente no método &lt;code>clean&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>Finalmente, a classe Meta do &lt;code>forms.py&lt;/code> ficou da seguinte forma:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl"> &lt;span class="k">class&lt;/span> &lt;span class="nc">Meta&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Fenomeno&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fields&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;nome&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;hora&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;latitude&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;longitude&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;geom&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">widgets&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;geom&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">HiddenInput&lt;/span>&lt;span class="p">()}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Pronto, com tudo isso que fizemos, já temos um sistema que, apesar de não poder fazer consultas espaciais, é capaz de validar os campos de &lt;code>latitude&lt;/code> e &lt;code>longitude&lt;/code>.&lt;/p>
&lt;p>No próximo artigo, vou abordar sobre o que está por trás de toda mágica de um webmap, usando o módulo &lt;code>django-leaflet&lt;/code>. Enquanto isso, dê uma olhada no que &lt;a href="http://felipesbarros.github.io/" target="_blank" rel="noopener">tenho desenvolvido&lt;/a>.&lt;/p></description></item><item><title>Criando um sistema para gestão de dados geográficos de forma simples e robusta II.</title><link>http://ambientalanalytics.com/post/geodjango2/</link><pubDate>Fri, 11 Jun 2021 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/post/geodjango2/</guid><description>&lt;p>&lt;em>Artigo publicado também no &lt;a href="https://www.linkedin.com/pulse/criando-um-sistema-para-gest%C3%A3o-de-dados-geogr%C3%A1ficos-e-felipe-/" target="_blank" rel="noopener">linkedin&lt;/a>.&lt;/em>&lt;/p>
&lt;p>Há algum tempo comecei a estudar sobre desenvolvimento de sistema com Python, usando a framework Django. Decidi expor alguns aprendizados em uma serie de artigos. A ideia é que esses textos me ajudem na consolidação do conhecimento e, ao tê-los publicado, ajudar a outros que tenham interesse na área.&lt;/p>
&lt;p>Aproveito para deixar meu agradecimento ao Cuducos que, tanto neste artigo, como em todos meus estudos tem sido um grande mentor. Vamos ao que interessa:&lt;/p>
&lt;p>Por simples, entende-se:&lt;/p>
&lt;p>Um sistema sem a necessidade da instalação e configuração de base de dados PostgreSQL/GIS, Geoserver, etc;
Um sistema clássico tipo Create, Retrieve, Update, Delete (CRUD) para dados geográficos;
Um sistema que não demande operações e consultas espaciais;
Mas um sistema que garanta a qualidade na gestão dos dados geográficos;&lt;/p>
&lt;h2 id="visão-geral-da-proposta">Visão geral da proposta:&lt;/h2>
&lt;p>Vamos criar um ambiente virtual Python e instalar a framework Django, para criar o sistema, assim como alguns módulos como &lt;a href="https://pypi.org/project/jsonfield/" target="_blank" rel="noopener">&lt;code>jsonfield&lt;/code>&lt;/a>, que nos vai habilitar a criação de campos &lt;code>JSON&lt;/code> em nossa base de dados; &lt;a href="https://pypi.org/project/django-geojson/" target="_blank" rel="noopener">&lt;code>django-geojson&lt;/code>&lt;/a>, que depende do &lt;code>jsonfield&lt;/code> e será responsável por habilitar instâncias de dados geográficos, baseando-se em &lt;code>JSON&lt;/code>; &lt;a href="https://pypi.org/project/geojson/" target="_blank" rel="noopener">&lt;code>geojson&lt;/code>&lt;/a>, que possui todas as regras básicas de validação de dados geográficos, usando a estrutura homônima, &lt;a href="https://geojson.org/" target="_blank" rel="noopener">&lt;code>geojson&lt;/code>&lt;/a>.&lt;/p>
&lt;p>O uso desses três módulos nos permitirá o desenvolvimento de um sistema de gestão de dados geográficos sem a necessidade de termos instalado um sistema de gerenciamento de dados geográficos, como o PostGIS. Sim, nosso sistema será bem limitado a algumas tarefas. Mas em contrapartida, poderemos desenvolvê-lo e implementar soluções “corriqueiras” de forma facilitada.&lt;/p>
&lt;p>No presente exemplo estarei usando &lt;a href="https://www.sqlite.org/index.html" target="_blank" rel="noopener">&lt;code>SQLite&lt;/code>&lt;/a>, como base de dados.&lt;/p>
&lt;p>Nosso projeto se chamará de map_proj. E nele vou criar uma app, dentro da pasta do meu projeto &lt;code>Django&lt;/code>, chamada &lt;code>core&lt;/code>. Essa organização e nomenclatura usada, vem das sugestões do &lt;a href="https://github.com/okfn-brasil/jarbas/issues/28#issuecomment-256117262" target="_blank" rel="noopener">Henrique Bastos&lt;/a>. Afinal, o sistema está nascendo. Ainda que eu tenha uma ideia do que ele será, é interessante iniciar com uma aplicação “genérica” e a partir do momento que o sistema se torne complexo, poderemos desacoplá-la em diferentes aplicações.&lt;/p>
&lt;h2 id="criando-ambiente-de-desenvolvimento-projeto-e-nossa-app">Criando ambiente de desenvolvimento, projeto e nossa app:&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">python -m venv .djleaflet # cria ambiente virtual python
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># ativando o ambiente virtual:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">source ./venv/bin/activate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># atualizando o pip
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install --upgrade pip
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># intalando os módulos a serem usados
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install django jsonfield django-geojson geojson
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># criando projeto
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">django-admin startproject map_proj .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># criando app dentro do projeto
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cd map_proj
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python manage.py startapp core
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># criando a base de dados inicial
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python manage.py migrate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># criando superusuário
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python manage.py createsuperuser
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="adicionando-os-módulos-e-a-app-ao-projeto">Adicionando os módulos e a app ao projeto&lt;/h3>
&lt;p>Agora é adicionar ao &lt;code>map_proj/settings.py&lt;/code>, a app criada e os módulos que usaremos.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># setting.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">INSTALLED_APPS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;djgeojson&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;map_proj.core&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Perceba que para poder acessar as classes de alto nível criadas pelo pacote &lt;code>djgeojson&lt;/code>, teremos que adicioná-lo ao &lt;code>INSTALLED_APPS&lt;/code> do &lt;code>settings.py&lt;/code>.&lt;/p>
&lt;h2 id="criando-a-base-de-dados">Criando a base de dados&lt;/h2>
&lt;p>Ainda que eu concorde com o Henrique Bastos, que a visão de começar os projetos Django pelo &lt;code>models.py&lt;/code> é um tanto “perigosa”, por colocar ênfase em uma parte da app e, em muitos casos, negligenciar vários outros atributos e ferramentas que o Django nos oferece, irei desconsiderar sua abordagem. Afinal, o objetivo deste artigo não é explorar todo o potencial do Django, mas sim apresentar uma solução simples no desenvolvimento e implementação de um sistema de gestão de dados geográficos para servir como ferramenta de estudo e projeto prático.&lt;/p>
&lt;p>Em &lt;code>models.py&lt;/code> usaremos instâncias de alto nível que o Django nos brinda para criar e configurar os campos e as tabelas que teremos em nosso sistema, bem como alguns comportamentos do sistema.&lt;/p>
&lt;p>Como estou desenvolvendo um sistema multi propósito, vou tentar mantê-lo bem genérico. A ideia é que vocês possam imaginar o que adequar para um sistema especialista na sua área de interesse. Vou criar, então, uma tabela para mapear “fenômenos” (quaisquer). Esse modelo terá os campos nome, data, hora e geometria, a qual será uma instância de &lt;code>PointField&lt;/code>.&lt;/p>
&lt;p>O &lt;code>PointField&lt;/code> é uma classe criada pelo &lt;code>djgeojson&lt;/code> que nos permite usar um campo para dados geográficos sem ter toda a infraestrutura do PostGIS, instalada, por exemplo. Nesse caso, estou simulando um campo de ponto, mas, de acordo com a documentação do pacote, todas as geometrias usadas em dados espaciais são suportadas:&lt;/p>
&lt;blockquote>
&lt;p>All geometry types are supported and respectively validated : GeometryField, PointField, MultiPointField, LineStringField, MultiLineStringField, PolygonField, MultiPolygonField, GeometryCollectionField. ( &lt;a href="https://django-geojson.readthedocs.io/en/latest/models.html" target="_blank" rel="noopener">djgeojson&lt;/a> )&lt;/p>
&lt;/blockquote>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># models.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">django.db&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">models&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">djgeojson.fields&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">PointField&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">Fenomeno&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">models&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Model&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nome&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">models&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">CharField&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">max_length&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">verbose_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Fenomeno mapeado&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">models&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">DateField&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">verbose_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Data da observação&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hora&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">models&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">TimeField&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">geom&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">PointField&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">blank&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__str__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">nome&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Percebam que eu importo de &lt;code>djgeojson&lt;/code> a classe &lt;code>PointField&lt;/code>. O que o &lt;code>django-geojson&lt;/code> fez foi criar uma classe [com estrutura de dados geográfico] de alto nível, mas que no banco de dados será armazenado em um campo &lt;code>JSON&lt;/code>. Vale a pena deixar claro: não espero que o usuário do meu sistema saiba preencher o campo &lt;code>geom&lt;/code> em formato &lt;code>JSON&lt;/code>. Por isso, criarei no &lt;code>forms.py&lt;/code>, os campos latitude e longitude e a partir deles, o campo geom será preenchido. Detalharei esse processo mais adiante.&lt;/p>
&lt;p>Pronto, já temos o modelo da ‘tabela de dados “geográficos”’, mas esse modelo ainda não foi registrado em nossa base. Para isso:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">python manage.py makemigrations
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python manage.py migrate
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>O &lt;code>makemigrations&lt;/code> analisa o &lt;code>models.py&lt;/code> e o compara com a versão anterior identificando as alterações e criando um arquivo que será executado pelo migrate, aplicando tais alterações ao banco de dados. Aprendi com o Henrique Bastos e &lt;a href="https://twitter.com/cuducos" target="_blank" rel="noopener">Cuducos&lt;/a> que o migrate é um sistema de versionamento da estrutura do banco de dados, permitindo retroceder, quando necessário, a outras versões.&lt;/p>
&lt;h2 id="criando-o-formulário">Criando o formulário&lt;/h2>
&lt;p>Vou aproveitar algumas “pilhas já incluídas” do Django, ao usar o &lt;code>ModelForm&lt;/code> para criar o formulário para o carregamento de dados. O &lt;code>ModelForm&lt;/code> facilita esse processo.&lt;/p>
&lt;p>Aliás, é importante pensar que os formulários do Django vão muito além da “carga de dados”, já que são os responsáveis por cuidar da interação com o usuário e o(s) processo(s) de validação e limpeza dos dados preenchidos.&lt;/p>
&lt;p>Digo isso, pois ao meu &lt;code>FenomenosForm&lt;/code>, eu sobreescrevo o método &lt;code>clean()&lt;/code>, que cuida da validação e limpeza do formulário e incluo nele:&lt;/p>
&lt;ol>
&lt;li>a construção dos dados do campo &lt;code>geom&lt;/code> a partir dos valores dos campos de latitude e longitude (criados exclusivamente para a gerção do campo geom);&lt;/li>
&lt;li>a validação do campo geom;&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># forms.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">django.core.exceptions&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">ValidationError&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">django.forms&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">ModelForm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">FloatField&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">map_proj.core.models&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Fenomeno&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">geojson&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Point&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">FenomenoForm&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ModelForm&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">longitude&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FloatField&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">latitude&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FloatField&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">class&lt;/span> &lt;span class="nc">Meta&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Fenomeno&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fields&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;nome&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;hora&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;latitude&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;longitude&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">clean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cleaned_data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">super&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">clean&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lon&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">cleaned_data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;longitude&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lat&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">cleaned_data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;latitude&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cleaned_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;geom&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Point&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">lon&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">lat&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">cleaned_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;geom&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">is_valid&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="n">ValidationError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Geometria inválida&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">cleaned_data&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ainda que pareça simples, não foi fácil chegar a essa estratégia de estruturação dos &lt;code>models&lt;/code> e &lt;code>forms&lt;/code>. Contei com a ajuda e paciencia do &lt;a href="https://twitter.com/cuducos" target="_blank" rel="noopener">Cuducos&lt;/a>. Inicialmente eu mantinha &lt;code>latitude&lt;/code> e &lt;code>longitude&lt;/code> no meu &lt;code>models&lt;/code>. Mas fazendo assim, além de ter uma redundância de dados e uma abertura a erros potenciais, estaria armazenando dados que não devo usar depois de contruir o campo geom. Uma alternativa, discutida com o Cuducos foi de ter tanto latitude como longitude no &lt;code>models&lt;/code>, mas o atributo &lt;code>geom&lt;/code> como &lt;a href="https://docs.python.org/3/howto/descriptor.html#properties" target="_blank" rel="noopener">&lt;code>propriedade&lt;/code>&lt;/a>. Ainda que seja uma estratégia consistente, a redundância se mantém.&lt;/p>
&lt;p>O processo de validação do campo &lt;code>geom&lt;/code> também foi fruto de muita discussão. De forma resumida, percebi que o &lt;code>djgeojson&lt;/code> apenas valida o tipo de geometria do campo e não a sua consistência. Ao conversar com os desenvolvedores, me disseram que toda a lógica de validação de objetos &lt;code>geojson&lt;/code> estavam sendo centralizados no módulo homônimo.&lt;/p>
&lt;p>Por isso eu carrego a classe &lt;code>Point&lt;/code> do módulo &lt;code>geojson&lt;/code> e designo o campo &lt;code>geom&lt;/code> como instância dessa classe. Assim, passo a poder contar com um processo de validação mais consistente, como o método &lt;code>is_valid&lt;/code>, usado anteriormente.&lt;/p>
&lt;h3 id="mas-e-o-teste">Mas e o teste?&lt;/h3>
&lt;p>Pois é, eu adoraria apresentar isso usando a abordagem &lt;em>Test Driven Development (TDD)&lt;/em>. Mas, talvez pela falta de prática, conhecimento e etc, vou apenas apontar onde e como eu testaria esse sistema. Faço isso como uma forma de estudo, mesmo. Também me pareceu complicado apresentar a abordagem TDD em um artigo, já que a mesma se faz de forma incremental.&lt;/p>
&lt;h4 id="sobre-tdd">Sobre TDD&lt;/h4>
&lt;p>Com o Henrique Bastos e toda a comunidade do &lt;a href="https://medium.com/welcome-to-the-django/o-wttd-%C3%A9-tudo-que-eu-ensinaria-sobre-prop%C3%B3sito-de-vida-para-mim-mesmo-se-pudesse-voltar-no-tempo-d73e516f911c" target="_blank" rel="noopener">Welcome to The Django&lt;/a> vi que essa abordagem é tanto filosófica quanto técnica. É praticamente “Chora agora, ri depois”, mas sem a parte de chorar. Pois com o tempo as coisas ficam mais claras… Alguns pontos:&lt;/p>
&lt;ul>
&lt;li>O erro não é para ser evitado no processo de desenvolvimento, mas sim quando estive em produção. Logo,&lt;/li>
&lt;li>Entenda o que você quer do sistema, crie um teste antes de implementar e deixe o erro te guiar até ter o que deseja;&lt;/li>
&lt;li>Teste o comportamento esperado e não cada elemento do sistema;&lt;/li>
&lt;/ul>
&lt;p>Sem mais delongas:&lt;/p>
&lt;h3 id="o-que-testar">O que testar?&lt;/h3>
&lt;p>Vamos usar o arquivo &lt;code>tests.py&lt;/code> e criar nossos testes lá. Ao abrir vocês vão ver que já está o comando importando o &lt;code>TestCase&lt;/code>.&lt;/p>
&lt;blockquote>
&lt;p>Mas o que vamos testar?&lt;/p>
&lt;/blockquote>
&lt;p>Como pretendo testar tanto a estrutura da minha base de dados, quanto o formulário e, de quebra, a validação do meu campo &lt;code>geom&lt;/code>, faço o import do modelo &lt;code>Fenomenos&lt;/code> e do form &lt;code>FenomenosForm&lt;/code>.&lt;/p>
&lt;p>⚠️ Essa não é uma boa prática. O ideal é criar uma pasta para os testes e separá-los em arquivos distintos. Um para cada elemento do sistema (model, form, view, etc).&lt;/p>
&lt;p>O primeiro teste será a carga de dados. Então, vou instanciar um objeto com o resultado da criação de um elemento do meu model &lt;code>Fenomeno&lt;/code>. Faço isso no &lt;code>setUp&lt;/code>, para não ter que criá-lo sempre que for fazer um teste relacionado à carga de dados.&lt;/p>
&lt;p>O teste seguinte será relacionado ao formulário e por isso instancio um formulário com os dados carregados e testo a sua validez. Ao fazer isso o formulário passa pelo processo de limpeza, onde está a construção e validação do campo &lt;code>geom&lt;/code>. Se qualquer campo for preenchido com dados errados ou inadequados, o django retornará &lt;code>False&lt;/code> ao método &lt;code>is_valid&lt;/code>. Ou seja, se eu tiver construido o campo &lt;code>geom&lt;/code> de forma equivocada, passando mais ou menos parâmetros que o esperado o nosso teste irá avisar, evitando surpresas.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># tests.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">django.test&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">TestCase&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">geojson&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Point&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">map_proj.core.models&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Fenomeno&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">map_proj.core.forms&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">FenomenoForm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">ModelGeomTest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TestCase&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">setUp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fenomeno&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Fenomeno&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">objects&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nome&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Arvore&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;2020-11-06&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hora&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;09:30:00&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_create&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertTrue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Fenomeno&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">objects&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">exists&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">FenomenoFormTest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TestCase&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">setUp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">form&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FenomenoForm&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;nome&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;Teste&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;2020-01-01&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;hora&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;09:12:12&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;longitude&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">45&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;latitude&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">22&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">validation&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">is_valid&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_form_is_valid&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;&amp;#34;form must be valid&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertTrue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">validation&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_geom_coordinates&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;after validating, geom have same values of longitude and latitude&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertEqual&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cleaned_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;geom&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">Point&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cleaned_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;longitude&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cleaned_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;latitude&amp;#39;&lt;/span>&lt;span class="p">])))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_geom_is_valid&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;geom must be valid&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assertTrue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cleaned_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;geom&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">is_valid&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>⚠️ Reparem que:&lt;/p>
&lt;ul>
&lt;li>No &lt;code>test_create()&lt;/code> eu testo se existem objetos inseridos no model &lt;code>Fenomeno&lt;/code>. Logo, testo se o dado criado no &lt;code>setUp&lt;/code> foi corretamente incorporado no banco de dados.&lt;/li>
&lt;li>Na classe &lt;code>FenomenosFormTest&lt;/code> eu crio uma instância do meu &lt;code>modelForm&lt;/code> e realizo três testes:
&lt;ul>
&lt;li>&lt;code>test_form_is_valid()&lt;/code> estou testando se os dados carregados são condizentes com o informado no model e, pelo fato desse método usar o método &lt;code>clean()&lt;/code>, posso dizer que estou testando indiretamente a validez do campo &lt;code>geom&lt;/code>. Caso ele não fosse válido, o form também não seria válido.&lt;/li>
&lt;li>Em &lt;code>test_geom_coordinates()&lt;/code> testo se após a validação o campo geom foi criado como esperado (como uma instância de &lt;code>Point&lt;/code> com os dalores de &lt;code>longitude&lt;/code> e &lt;code>latitude&lt;/code>).&lt;/li>
&lt;li>O teste &lt;code>test_geom_is_valid()&lt;/code> serve para garantir que a contrução do campo &lt;code>geom&lt;/code> é valido. Ainda que ao testar se o formulário é valido eu estaria implicitamente testando a validez do campo &lt;code>geom&lt;/code>, esse teste serve para garantir a criação válida do campo. Afinal, por algum motivo (como por exemplo, refatoração), pode ser que façamos alguma alteração no método &lt;code>clean()&lt;/code> que mantenha o formulário como válido mas deixe de garantir a validez do campo &lt;code>geom&lt;/code>.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>A diferença entre as classes de teste criadas está no fato de ao inserir os dados usando o método &lt;code>create()&lt;/code> - e aconteceria o mesmo se estivesse usando o &lt;code>save()&lt;/code> -, apenas será validado se o elemento a ser inserido é condizente com o tipo de coluna no banco de dados. Vale deixar claro: Dessa forma, eu não estou validando a consistência do campo &lt;code>geom&lt;/code>, já que o mesmo, caso seja informado, será salvo com sucesso sempre que represente um &lt;code>JSON&lt;/code>.&lt;/p>
&lt;p>Esse fato é importante para reforçar o entendimento de que o &lt;code>djgeojson&lt;/code> implementa classes de alto nível a serem trabalhados em &lt;code>views&lt;/code> e &lt;code>models&lt;/code>. No banco, mesmo, temos um campo de &lt;code>JSON&lt;/code>. Enquanto que, para poder validar a consistência do campo &lt;code>geom&lt;/code>, preciso passar os dados pelo formulário onde, no processo de limpeza do mesmo, o campo será criado e validado usando o módulo &lt;code>geojson&lt;/code>. Por isso a classe com os testes relacionados ao comportamento do formulário.&lt;/p>
&lt;h2 id="registrando-modelo-no-admin">Registrando modelo no admin&lt;/h2>
&lt;p>Para facilitar, vou usar o django-admin. Trata-se de uma aplicação já criada onde basta registrar os modelos e views que estamos trabalhando para termos uma interface “frontend” genérica.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#admin.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">django.contrib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">admin&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">map_proj.core.models&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Fenomeno&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">map_proj.core.forms&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">FenomenoForm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">FenomenoAdmin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">admin&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ModelAdmin&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Fenomeno&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">form&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FenomenoForm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">admin&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">site&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">register&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Fenomeno&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">FenomenoAdmin&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="to-be-continued">To be continued…&lt;/h2>
&lt;p>Até o momento já temos algo bastante interessante: um sistema de CRUD que nos permite adicionar, editar e remover dados geográficos. Talvez você esteja pensando consigo mesmo:&lt;/p>
&lt;blockquote>
&lt;p>“OK. Mas o que foi feito até agora, poderia ter sido feito basicamente com uma base de dados que possuam as colunas latitude e longitude”.&lt;/p>
&lt;/blockquote>
&lt;p>Eu diria que sim, até certo ponto. Uma grande diferença, eu diria, da forma como foi implementada é o uso das ferramentas de validação dos dados com o módulo &lt;code>geojson&lt;/code>.&lt;/p>
&lt;p>A ideia é, a seguir (e seja lá quando isso for), extender a funcionalidade do sistema ao implementar um webmap para visualizar os dados mapeados.&lt;/p></description></item><item><title>Hacktoberfest: Colaborações e aprendizados.</title><link>http://ambientalanalytics.com/post/hacktoberfest21/</link><pubDate>Fri, 21 May 2021 00:00:00 +0000</pubDate><guid>http://ambientalanalytics.com/post/hacktoberfest21/</guid><description>&lt;p>&lt;em>Artigo publicado também no &lt;a href="https://www.linkedin.com/pulse/hacktoberfest-colabora%C3%A7%C3%B5es-e-aprendizados-felipe-sodr%C3%A9-mendes-barros/" target="_blank" rel="noopener">linkedin&lt;/a>.&lt;/em>&lt;/p>
&lt;p>O &lt;a href="https://hacktoberfest.digitalocean.com/" target="_blank" rel="noopener">HacktoberFest&lt;/a> é um evento promovido pela Digital Ocean durante o mês de outubro e já está na sua oitava edição. O objetivo é incentivar a colaboração em projetos de código aberto e, claro, como uma forma de democratizar o conhecimento em sistemas de versionamento, como o &lt;a href="https://git-scm.com/" target="_blank" rel="noopener">git&lt;/a>, além de outras tecnologias.&lt;/p>
&lt;blockquote>
&lt;p>…Ah, e o incentivo vem com a possibilidade de ganhar uma camisa do evento ao ter aprovado quatro &lt;a href="https://git-scm.com/docs/git-request-pull" target="_blank" rel="noopener">pull requests&lt;/a> em repositórios participantes (para participar, basta adicionar a tag “Hactoberfest” ao repositório ou adicionar a tag “Hacktoberfest-accepted” no Pull request em questão).&lt;/p>
&lt;/blockquote>
&lt;p>Não foi minha primeira participação, mas foi a primeira vez que pude colaborar em projetos diferentes daqueles relacionados ao meu trabalho cotidiano. E já fazia algum tempo que tinha interesse em colaborar, mas não sabia como quebrar a inércia. Compartilho neste artigo, alguns projetos desenvolvidos este ano e o que pude aprender nos mesmos.&lt;/p>
&lt;h2 id="fogo-cruzado">Fogo Cruzado&lt;/h2>
&lt;p>O projeto &lt;a href="https://fogocruzado.org.br/" target="_blank" rel="noopener">Fogo Cruzado&lt;/a> foi desenvolvido pela &lt;a href="https://voltdata.info/" target="_blank" rel="noopener">Volt Data Lab&lt;/a> e &lt;a href="https://twitter.com/fogocruzado" target="_blank" rel="noopener">Instituto Fogo Cruzado&lt;/a>, como um sistema &lt;a href="https://pt.wikipedia.org/wiki/Crowdsourcing" target="_blank" rel="noopener">Crowdsourcing&lt;/a> para monitoramento dos tiroteios no Rio de Janeiro e/ou em Recife. O mesmo &lt;a href="https://fogocruzado.org.br/sobre-a-api/" target="_blank" rel="noopener">disponibiliza uma API&lt;/a> para acessar aos dados, bastando criar um usuário, sem custo. E o projeto já tem um pacote para acessar os dados pelo &lt;a href="https://github.com/voltdatalab/crossfire" target="_blank" rel="noopener">R&lt;/a>.&lt;/p>
&lt;p>Como faltava um módulo python para acessar os dados do projeto, decidi fazê-lo durante o #Hacktoberfest. Esse foi o primeiro projeto: um módulo python para acessar os dados da API, direto do python.&lt;/p>
&lt;p>Foi um desafio legal e, até certa forma, simples, pois eu já tinha um modelo de como funcionava o pacote em R. Então o trabalho foi, principalmente “traduzir” ao python. Com isso aproveitei para refatorar algumas partes do código.&lt;/p>
&lt;p>No geral, posso dizer que ao desenvolver esse projeto, aprendi sobre:&lt;/p>
&lt;ul>
&lt;li>Python-poetry (do zero);&lt;/li>
&lt;li>Validação de login usando variáveis do sistema;&lt;/li>
&lt;li>Publicação de módulos no PyPi;&lt;/li>
&lt;/ul>
&lt;h3 id="e-fica-como-desafios-para-melhorimplementar-em-breve">E fica como desafios para melhor/implementar em breve:&lt;/h3>
&lt;ul>
&lt;li>Melhorar o código com &lt;code>type annotation&lt;/code>;&lt;/li>
&lt;li>Criação de documentação com &lt;code>Sphynx&lt;/code> (se alguém quiser sugerir outra alternativa será bem-vinda);&lt;/li>
&lt;/ul>
&lt;h2 id="pyinaturalist-convert">PyInaturalist-convert&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="" srcset="
/post/hacktoberfest21/pyinaturalist_logo_med_hudb846e672b2679476c96ab2f50adb3f9_57635_2369fb4c9bc9c0fa0bfb596a60d916d0.webp 400w,
/post/hacktoberfest21/pyinaturalist_logo_med_hudb846e672b2679476c96ab2f50adb3f9_57635_ce571a69a133320dc0c1ecdab6baddae.webp 760w,
/post/hacktoberfest21/pyinaturalist_logo_med_hudb846e672b2679476c96ab2f50adb3f9_57635_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="http://ambientalanalytics.com/post/hacktoberfest21/pyinaturalist_logo_med_hudb846e672b2679476c96ab2f50adb3f9_57635_2369fb4c9bc9c0fa0bfb596a60d916d0.webp"
width="760"
height="102"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>O segundo projeto que atuei nesse mês foi no &lt;a href="https://github.com/JWCook/pyinaturalist-convert" target="_blank" rel="noopener">PyInaturalist-convert&lt;/a>. A história deste módulo é bem interessante e surge de uma demanda pessoal: O &lt;a href="https://imibio.misiones.gob.ar/" target="_blank" rel="noopener">IMiBio&lt;/a>, Instituição onde trabalho, está desenvolvendo um projeto com o &lt;a href="https://www.inaturalist.org/" target="_blank" rel="noopener">INaturalist&lt;/a>, uma aplicação de &lt;a href="https://pt.wikipedia.org/wiki/Crowdsourcing" target="_blank" rel="noopener">crowdsourcing&lt;/a> para observação de biodiversidade, e eu tive que criar um sistema que acesse os dados do projeto usando a &lt;a href="https://github.com/inaturalist/iNaturalistAPI" target="_blank" rel="noopener">API deles&lt;/a>. Com isso, conheci o módulo &lt;a href="https://github.com/niconoe/pyinaturalist" target="_blank" rel="noopener">PyINaturalist&lt;/a>. Conversando com os desenvolvedores, comentei que seria interessante ter os dados no padrão &lt;a href="https://dwc.tdwg.org/" target="_blank" rel="noopener">darwincore&lt;/a>. Um deles achou pertinente e começamos a desenvolver juntos. Contudo, fiquei uns bons meses afastado do projeto e ao voltar, já era um pacote bem estruturado. Por isso, para entender a estrutura do mesmo e saber por onde começar, além de ler as &lt;a href="https://guides.github.com/features/issues/" target="_blank" rel="noopener">issues&lt;/a> abertas, adotei a estratégia de ler os testes…&lt;/p>
&lt;p>… E foi lendo os testes que percebi que estavam implementando um objeto &lt;code>geojson&lt;/code>, “na unha”. Como estive estudando sobre o &lt;code>geojson&lt;/code> (e, inclusive foi um dos temas explorados por mim em &lt;a href="https://felipesbarros.github.io/post/criando-um-sistema-para-gestao-de-dados-geograficos-de-forma-simples-e-robusta-ii/" target="_blank" rel="noopener">outros artigos&lt;/a>, propus usá-lo. Com isso, poderíamos usar os métodos de validação já implementados no módulo, garantindo consistência aos dados;&lt;/p>
&lt;p>Ao colaborar no módulo &lt;code>PyInaturalist-converter&lt;/code>, aprendi sobre:&lt;/p>
&lt;ul>
&lt;li>Como colaborar a um projeto já estruturado. Tenho certeza que essa não é uma regra. Mas foi uma boa estratégia começar lendo os testes;&lt;/li>
&lt;li>Mais aprendizados sobre python-poetry :);&lt;/li>
&lt;li>Soube da existência do formatador de código &lt;code>Black&lt;/code>;&lt;/li>
&lt;li>Soube da existencia do &lt;a href="https://pycqa.github.io/isort/" target="_blank" rel="noopener">ISORT&lt;/a>, para padronizar os &lt;code>imports&lt;/code>;&lt;/li>
&lt;/ul>
&lt;p>Além desses aprendizados, o autor principal do módulo já tinha configurado no repositório um &lt;a href="https://docs.github.com/pt/actions" target="_blank" rel="noopener">fluxo de ações&lt;/a> e validações bem interessantes. Dessa forma, havia um sistema de validação do que se estava propondo como &lt;code>pull request&lt;/code>. Ainda não tive tempo de me aprofundar, mas já está na lista de estudos futuros…&lt;/p>
&lt;h2 id="análise-espacial-no-frontend">Análise espacial no frontend&lt;/h2>
&lt;p>Uma última atividade que queria compartilhar, não está relacionada a uma contribuição minha, mas sim, um projeto ao qual eu recebi ajuda.&lt;/p>
&lt;p>&lt;a href="https://www.linkedin.com/pulse/an%C3%A1lise-espacial-frontend-felipe-sodr%C3%A9-mendes-barros?trk=public_post-content_share-article" target="_blank" rel="noopener">Publiquei recentemente&lt;/a> um artigo apresentando alguns módulos de JavaScript que nos permitem fazer algumas análises espaciais sem depender de uma infraestrutura de servidores de dados e de mapas.&lt;/p>
&lt;p>Eu fui apresentado a essas tecnologias no &lt;a href="https://2021.foss4g.org/" target="_blank" rel="noopener">FOSS4G 2021&lt;/a> e por pura curiosidade, já que frontend não é a “minha praia”, comecei a fazer alguns testes como estratégia de estudos, mesmo.&lt;/p>
&lt;p>Pude evoluir bastante com os estudos, mas num momento vi que poderia ser feito muito mais, mas que eu não tinha conhecimento técnico em JS para isso. Não tive dúvidas em contactar um amigo que trabalha com JS e apresentei a ele o que estava tentando fazer. Ele curtiu e acabou colaborando com o projeto, transformando essa prova de conceito numa solução, em algo realmente interessante.&lt;/p>
&lt;p>Percebam que essa colaboração não surgiu pelo Hacktoberfest. Mas por uma mudança de postura minha em me conectar com outras pessoas e apresentar o que eu estava estudando, as “minhas dores” e o que pretendia fazer.&lt;/p>
&lt;p>Neste projeto estudei e aprendi sobre:&lt;/p>
&lt;ul>
&lt;li>O &lt;code>georaster&lt;/code>, uma biblioteca JavaScript que nos permite carrregar, e até mesmo criar, dados raster a partir de objetos JavaScript;&lt;/li>
&lt;li>&lt;code>georaster-layer-for-leaflet&lt;/code> que é uma biblioteca que nos permite apresentar dados &lt;code>raster&lt;/code> (a princípio geotif) nos mapas feitos em &lt;code>leaflet&lt;/code>;&lt;/li>
&lt;li>&lt;code>geoblaze&lt;/code> que é um pacote desenvolvido em JavaScript para permitir analisar dados carregados como georaster.&lt;/li>
&lt;/ul>
&lt;p>Como resultado, criamos dois visualizadores de dados raster apenas com tecnologia frontend. &lt;a href="https://felipesbarros.github.io/geoblaze_test/clicking_pixel" target="_blank" rel="noopener">No primeiro o usuário interage com o pixel&lt;/a> (clicando num píxel específico) e o gráfico apresenta o comportamento temporal daquele pixel; &lt;a href="https://felipesbarros.github.io/geoblaze_test/clicking_polygon" target="_blank" rel="noopener">No segundo visualizador o usuário clica em um dos estados&lt;/a> e o gráfico apresenta o valor médio dos pixels daquele estado ao longo do tempo.&lt;/p>
&lt;h2 id="notas-finais-sobre-hacktoberfest">Notas finais sobre Hacktoberfest&lt;/h2>
&lt;p>Entendo que muitos “torcem o nariz” para o Hacktoberfest, pois poucos o utilizam como uma estratégia de estudos, crescimento ou colaboração a projeto de código aberto, que são os objetivos principais. A ideia de escrever sobre as colaborações feitas é justamente destacar que o evento é uma grande estratégia/ferramenta para aprender mais, conectar-se com outros desenvolvedores e se engajar em projetos de código aberto.&lt;/p>
&lt;p>Espero que os meus aprendizados serviam de motivação aos demais. Qualquer coisa, fico à disposição para conversar mais a respeito.&lt;/p></description></item></channel></rss>