<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Paul Götze</title>
    <description>The latest articles on DEV Community by Paul Götze (@paulgoetze).</description>
    <link>https://dev.to/paulgoetze</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F119192%2F2f3e5a64-2945-4970-847e-99d8c42c21ab.png</url>
      <title>DEV Community: Paul Götze</title>
      <link>https://dev.to/paulgoetze</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paulgoetze"/>
    <language>en</language>
    <item>
      <title>The &lt;details&gt; Of a Dropdown</title>
      <dc:creator>Paul Götze</dc:creator>
      <pubDate>Sat, 30 Apr 2022 08:49:33 +0000</pubDate>
      <link>https://dev.to/paulgoetze/the-of-a-dropdown-12kp</link>
      <guid>https://dev.to/paulgoetze/the-of-a-dropdown-12kp</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;How to build a beautiful dropdown panel with just HTML and CSS&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I would by no means consider myself an expert when it comes to HTML &amp;amp; CSS. But I thought I just share what I found to work really well for me, when I tried to build a dropdown panel without any JavaScript.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt; You can build a fully functional dropdown panel by using plain HTML and CSS with no JavaScript involved by leveraging the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; tags. Here’s a minimal example that you can use and customize to your needs:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/paulgoetze/embed/NWGaZPP?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;If you want to see more advanced dropdowns built with this approach and want to learn about what’s going on here, then read on.&lt;/p&gt;




&lt;p&gt;There is an HTML tag that might count among the little known but most powerful tags in the world of websites: the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;It usually appears together with another tag that makes the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; tag shine: the &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;It basically does what it says — it shows you a summary. And you can click on it to see some details. The best feature is, that it comes out of the box with each browser, no JavaScript needed for toggling the details. Just plain old HTML. Here’s what it looks like by default:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/paulgoetze/embed/RwWLQqe?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Wow, not sparking too much joy, yet. But we’ll get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Spice It Up 🌶️
&lt;/h2&gt;

&lt;p&gt;With a bit of styling, the plain &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; snippet from above can already be used for such exciting things as questions (summary) and answers (details) on your &lt;a href="https://adoptoposs.org/faq" rel="noopener noreferrer"&gt;FAQ page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But let us explore some even more exciting use cases. Let’s build a dropdown menu that might live in the navigation on your web page.&lt;/p&gt;

&lt;p&gt;First, we put a container around our &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; snippet which will represent the dropdown element. Then we replace the former summary text with a hamburger menu icon and insert a list as the actual dropdown content. Last, we wrap the whole dropdown with a &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; tag and give all of that some quick color styles. Et voilà:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/paulgoetze/embed/gOaGepN?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Hiding The Disclosure Widget
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; tag comes with an &lt;code&gt;open&lt;/code&gt; attribute and a disclosure widget (▶, ▼) which indicates whether the dropdown is opened and details are visible. Let’s hide this marker, so that the click area looks more like a general button.&lt;/p&gt;

&lt;p&gt;In Firefox we can do so by setting &lt;code&gt;list-style: none;&lt;/code&gt; for our summary. In other browsers you need to apply &lt;code&gt;display: none;&lt;/code&gt; for the summary’s pseudo-class &lt;code&gt;::-webkit-details-marker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We’ll also fix the cursor behavior for the &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; on the fly. So, this is what we end up with after applying these fixes:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/paulgoetze/embed/pojWOEK?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Detaching The Details
&lt;/h2&gt;

&lt;p&gt;This might already serve well as a simple dropdown menu. But we can do much better.&lt;/p&gt;

&lt;p&gt;Right now, when opening the details, the content enlarges the details container and therefore also pushes down any other content that lives below our navigation bar. In more complex navigations we might like to have a dropdown panel that is detached from the triggering summary.&lt;/p&gt;

&lt;p&gt;So, next up, we’ll add some styles to display the actual menu content in a position-wise independent panel.&lt;/p&gt;

&lt;p&gt;We can get an independent menu content by adjusting its position property. Hence, we wrap our content into a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; container and give it an absolute position. With this, the opened menu content is now displayed directly below the summary, which is the opening trigger for our dropdown menu.&lt;/p&gt;

&lt;p&gt;In order to define if the panel is opened to the left or the right of the triggering summary, we apply a &lt;code&gt;position: relative;&lt;/code&gt; for the dropdown &lt;/p&gt; container. You can now customize the dropdown panel’s anchor by applying &lt;code&gt;right: 0;&lt;/code&gt; or &lt;code&gt;left: 0;&lt;/code&gt; (which is the default) to the summary.

&lt;p&gt;When using a relative position, we also need to make sure the dropdown panel has a minimum inline size of its maximum content width – else our menu items will have unwanted line breaks.&lt;/p&gt;

&lt;p&gt;By giving the dropdown &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; container an additional &lt;code&gt;display: inline-block;&lt;/code&gt; we make sure the dropdown panel only opens when clicking the hamburger menu icon directly.&lt;br&gt;
Similar to before, we also add some list styles to make it already look like a menu and make it more distinct from the navigation and the text content below:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/paulgoetze/embed/qBOPMPE?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing The Menu
&lt;/h2&gt;

&lt;p&gt;Opening and closing the dropdown works nicely and it looks like a navigation menu alright. However, there’s still an issue if we open our menu and decide to not click anything in it but leave for interacting with other content on the page. Then our menu will still be wide open and cover underlying content.&lt;/p&gt;

&lt;p&gt;So, we need to figure out how to close the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; again, whenever we click somewhere else outside the menu area.&lt;/p&gt;

&lt;p&gt;We can use a neat little trick to reach this behavior — again without any JavaScript involved. When the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; container is in the open state, then clicking any &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; content will hide the menu content.&lt;/p&gt;

&lt;p&gt;We can leverage this behavior by making sure, that the only area we can click on outside the menu content will always be the &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; area. So, a click anywhere else on the page would always trigger closing the menu.&lt;/p&gt;

&lt;p&gt;Technically this is possible by enlarging the summaries &lt;code&gt;::before&lt;/code&gt; pseudo-class to the full view size. This is done by giving it a &lt;code&gt;fixed&lt;/code&gt; position and expanding it to all four view corners (setting &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, &lt;code&gt;bottom&lt;/code&gt;, and &lt;code&gt;left&lt;/code&gt; to 0).&lt;/p&gt;

&lt;p&gt;In order to fill the whole screen we also need to set the &lt;code&gt;content&lt;/code&gt; of the &lt;code&gt;::before&lt;/code&gt; pseudo-class. By default the details content is displayed with a higher z-index than the related summary content, so we don’t need to care about this. You can still interact with the menu content.&lt;/p&gt;

&lt;p&gt;The next example applies a transparent background color for the &lt;code&gt;summary::before&lt;/code&gt;’s content, so that we can see the area covering the entire screen behind the menu content:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/paulgoetze/embed/bGVoxyj?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This approach can be used for a multitude of different dropdown panels, including navigation menus, sharing widgets, and all sorts of buttons that open a panel with further details or actions.&lt;/p&gt;

&lt;p&gt;In fact, GitHub is using a similar approach for their clone button and the branch select panel — which is also where I took inspiration from to kind of reverse-engineer the described approach:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nr2w099ayh8abbb9vro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nr2w099ayh8abbb9vro.png" alt="GitHub’s “Code” &amp;amp; “select branch” dropdown is built using &amp;lt;details&amp;gt; and &amp;lt;summary&amp;gt; tags&amp;lt;br&amp;gt;
"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;GitHub’s “Code” &amp;amp; “select branch” dropdown is built using &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; tags&lt;br&gt;
&lt;small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;With some minor additions and a couple more CSS styles we can make our example into a shiny dropdown menu, that works just as you would expect a dropdown menu to work:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/paulgoetze/embed/OJyxByw?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;As long as you don’t want to put any more complex interactions into the dropdown, the described approach does not need any JavaScript.&lt;/p&gt;

&lt;p&gt;However, if you don’t have a page reload after clicking a link in the dropdown panel, you would need to add some JavaScript to toggle the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;s open state. The same applies for any additional actions from within the dropdown panel that should change its open state.&lt;/p&gt;

&lt;p&gt;I hope you learned some useful details about how to built dropdowns. We can give the positive summary that you might not always need JavaScript to build interactive web components.&lt;/p&gt;

&lt;p&gt;HTML and CSS might have you covered in more cases than you think. For dropdown menus and dropdown panels it certainly does.&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;br&gt;
&lt;em&gt;This post was originally posted on my personal blog — Easter egg included ;)&lt;/em&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://paulgoetze.com/2020/05/09/the-details-of-a-dropdown" rel="noopener noreferrer"&gt;https://paulgoetze.com/2020/05/09/the-details-of-a-dropdown&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;/small&gt;&lt;br&gt;
&lt;small&gt;&lt;br&gt;
Credits: header image by &lt;a href="https://unsplash.com/photos/AcVTl5lMpFY?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditShareLink" rel="noopener noreferrer"&gt;Fabrizio Conti&lt;/a&gt;.&lt;small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>tutorial</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Testing your JSON API in Ruby with dry-rb</title>
      <dc:creator>Paul Götze</dc:creator>
      <pubDate>Fri, 29 Apr 2022 12:21:42 +0000</pubDate>
      <link>https://dev.to/paulgoetze/testing-your-json-api-in-ruby-with-dry-rb-410p</link>
      <guid>https://dev.to/paulgoetze/testing-your-json-api-in-ruby-with-dry-rb-410p</guid>
      <description>&lt;p&gt;When writing a JSON API with Ruby in your favorite web framework, you will certainly come to a point, where you have to decide how to test your endpoints. In this article I’m going to show one of my preferred ways to test the structure of JSON responses in a clear and readable way—by making use of the dry-schema gem and some other dry-rb libraries.&lt;/p&gt;




&lt;h2&gt;
  
  
  There’s Different Ways
&lt;/h2&gt;

&lt;p&gt;When it comes to testing your JSON API endpoints in Ruby, you can go for plenty of different approaches. The simplest probably is to check the status code and explicitly assert each value of you response data.&lt;/p&gt;

&lt;p&gt;Let’s assume we want to test a &lt;code&gt;GET /todos&lt;/code&gt; endpoint, which returns to-do items like this:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Then a simple test for this response could look like this:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;However, this approach is a bit cumbersome for large response bodies and can result in a lot of accesses to nested hash keys, until you get to the value you actually want to check. It will also take you quite some effort to adjust the tests as soon as some keys or values in the JSON response change.&lt;/p&gt;

&lt;p&gt;So, instead of checking for each exact value, you could move up one level of abstraction and only check for the right structure and data types in your response body. Again there’s different ways to do that.&lt;/p&gt;

&lt;p&gt;You could use JSON Schema files and validate your response against them, as is described here:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
      &lt;div class="c-embed__cover"&gt;
        &lt;a href="https://thoughtbot.com/blog/validating-json-schemas-with-an-rspec-matcher" class="c-link s:max-w-50 align-middle" rel="noopener noreferrer"&gt;
          &lt;img alt="" src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.prismic.io%2Fthoughtbot-website%2FZn0Q2JbWFbowe7qY_default-article-background.png%3Fauto%3Dformat%252Ccompress%26mark-x%3D356%26mark-y%3D100%26mark64%3DaHR0cHM6Ly9hc3NldHMuaW1naXgubmV0L350ZXh0Lz90eHQtbGVhZD0wJnR4dC10cmFjaz0wJnR4dDY0PVZtRnNhV1JoZEdsdVp5QktVMDlPSUZOamFHVnRZWE1nZDJsMGFDQmhiaUJTVTNCbFl5Qk5ZWFJqYUdWeSZ0eHRjbHI9ZjVmNWY1JnR4dGZvbnQ9SUJNUGxleFNhbnNKUC1TZW1pQm9sZCZ0eHRwYWQ9MCZ0eHRzaXplPTY0Jnc9ODAw%26txt-align%3Dcenter%252Cmiddle%26txt-color%3Df5f5f5%26txt-fit%3Dmax%26txt-font%3DIBMPlexSansJP-SemiBold%26txt-size%3D24%26txt-x%3D391%26txt-y%3D526%26txt%3DLaila%2BWinner" height="auto" class="m-0"&gt;
        &lt;/a&gt;
      &lt;/div&gt;
    &lt;div class="c-embed__body"&gt;
      &lt;h2 class="fs-xl lh-tight"&gt;
        &lt;a href="https://thoughtbot.com/blog/validating-json-schemas-with-an-rspec-matcher" rel="noopener noreferrer" class="c-link"&gt;
          Validating JSON Schemas with an RSpec Matcher
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;p class="truncate-at-3"&gt;
          Use RSpec and JSON Schema to create a test-driven process in which changes to the structure of your JSON API drive the implementation of new features.
        &lt;/p&gt;
      &lt;div class="color-secondary fs-s flex items-center"&gt;
          &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthoughtbot.com%2Fassets%2Ffavicon-c1fa98a84eab9d930e8b09cf6a4dbb1156d1436c25c225e6e14fcc5cc84d1b34.ico"&gt;
        thoughtbot.com
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The JSON Schema approach works quite well. However, writing JSON Schema files can get a bit tricky and confusing, especially when you have to deal with complex and nested JSON data. There’s ways to handle this complexity, e.g. by splitting up and &lt;a href="https://medium.com/grammofy/handling-complex-json-schemas-in-python-9eacc04a60cf?sk=2acab57dc699857181742e153f3e58bb" rel="noopener noreferrer"&gt;reusing schemas by referencing&lt;/a&gt; them.&lt;/p&gt;

&lt;p&gt;But maybe there’s a more legible and maintainable way than JSON Schema validation. Let’s explore the realm of dry-rb for this purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is dry-rb?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dry-rb.org/" rel="noopener noreferrer"&gt;dry-rb&lt;/a&gt; is a collection of Ruby libraries, whose goal is to encapsulate common tasks–like data validation, data transformation, Ruby object initialization, and others–in small, reusable, and framework-independent Ruby gems.&lt;/p&gt;

&lt;p&gt;For our purpose of validating the structure and types of JSON data it provides us with the &lt;a href="https://dry-rb.org/gems/dry-validation" rel="noopener noreferrer"&gt;dry-validation&lt;/a&gt;, &lt;a href="https://dry-rb.org/gems/dry-schema" rel="noopener noreferrer"&gt;dry-schema&lt;/a&gt;, and &lt;a href="https://dry-rb.org/gems/dry-types" rel="noopener noreferrer"&gt;dry-types&lt;/a&gt; gems. These gems give us all we need for setting up a schema to validate JSON data of almost any complexity.&lt;/p&gt;

&lt;p&gt;So let’s have a look at some validation use cases and let’s see how we can leverage the mentioned dry-rb gems for a simple JSON structure. We will also dig into a more complex one afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple JSON Response
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://dry-rb.org/gems/dry-schema" rel="noopener noreferrer"&gt;dry-schema&lt;/a&gt; gem comes with a &lt;code&gt;Dry::Schema&lt;/code&gt; module which has a &lt;code&gt;Params&lt;/code&gt; method to define schemas for validating hashes. Built on top, it also has a &lt;code&gt;JSON&lt;/code&gt; method for defining JSON schemas with coercible data types, like &lt;code&gt;Date&lt;/code&gt; or &lt;code&gt;Time&lt;/code&gt;. This is the one we want to use for our response validation.&lt;/p&gt;

&lt;p&gt;A schema for one of our to-do items from above would look something like this:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;With these definitions we make sure that an &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, and the &lt;code&gt;done&lt;/code&gt; flag is present in our to-do item JSON object–and that each property has the given type.&lt;/p&gt;

&lt;p&gt;Instead of &lt;code&gt;required()&lt;/code&gt; we can also use &lt;code&gt;optional()&lt;/code&gt;, to allow the object to not have this property. Instead of &lt;code&gt;value()&lt;/code&gt; we can also put &lt;code&gt;maybe()&lt;/code&gt; to allow nil values.&lt;/p&gt;

&lt;p&gt;Having this schema defined, we can now run the validation in our test for each to-do item by calling the schema, instead of checking for each value explicitly as we did before:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Dry-schema also allows us to nest schemas and to pass schemas as property types. So, let’s build a schema for our entire JSON response:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Putting all of this together in our test case we can now validate our response body to match the &lt;code&gt;ResponseSchema&lt;/code&gt;:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Of course you might still want to check some of the JSON data’s properties explicitly, e.g. to make sure the right to-do ids are involved.&lt;/p&gt;

&lt;p&gt;Nevertheless, using dry-schemas to validate the response structure makes your test code a bit more concise already and also makes it easier to understand what the actual JSON response looks like. You can have a quick look at the involved schemas to get an idea about required or optional properties and their (maybe nullable) types.&lt;/p&gt;

&lt;h2&gt;
  
  
  A More Complex JSON Response
&lt;/h2&gt;

&lt;p&gt;You might think: “Well, nice little examples, but my real-world JSON data is way more complex. Can I use this for these structures, too?”.&lt;/p&gt;

&lt;p&gt;I think you can. Let’s see how.&lt;/p&gt;

&lt;p&gt;Let’s replace some parts of our simple to-do JSON with more complex ones. Instead of the &lt;code&gt;done&lt;/code&gt; flag we will have a &lt;code&gt;done_at&lt;/code&gt; timestamp, we will add a &lt;code&gt;created_at&lt;/code&gt; timestamp and a &lt;code&gt;priority&lt;/code&gt; (which could be one of &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;default&lt;/code&gt;, or &lt;code&gt;low&lt;/code&gt;). We will also assume that we have a collaborative to-do list, where we want to include the creator and assignee for each to-do item. Last, let’s have an arbitrary list of tags that can be assigned for a to-do item. This is how our new JSON response might look like:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;A very basic schema for this response would be something like the following:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Note that we used &lt;code&gt;maybe()&lt;/code&gt; for the &lt;code&gt;done_at&lt;/code&gt; property, which allows to have nil values.&lt;/p&gt;

&lt;p&gt;This only validates the presence and the top level type of each property. The &lt;a href="https://dry-rb.org/gems/dry-types" rel="noopener noreferrer"&gt;dry-types&lt;/a&gt; gem that is used by our schema, however, provides us with a number of built-in types, like &lt;code&gt;Types::JSON::DateTime&lt;/code&gt; (which will be used under the hood if we type &lt;code&gt;:date_time&lt;/code&gt;), &lt;code&gt;Types::Array&lt;/code&gt;, and &lt;code&gt;Types::String.enum&lt;/code&gt;. We can use these to refine our timestamps and tags types in the schema. In order to create custom types we need to create a &lt;code&gt;Types&lt;/code&gt; module and include the dry types. Similar to what we already saw for the simple JSON response, we can also create and reuse sub-schemas for the &lt;code&gt;creator&lt;/code&gt; and &lt;code&gt;assignee&lt;/code&gt; properties:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;This now looks already more like a real-world use case!&lt;/p&gt;

&lt;p&gt;Still, we can go one step further and check for the integrity of certain properties. With &lt;a href="https://dry-rb.org/gems/dry-validation" rel="noopener noreferrer"&gt;dry-validation&lt;/a&gt; (which is powered by dry-schema) we have a tool at hand, that allows us to define custom rules for our JSON data on top of our existing schema definitions.&lt;/p&gt;

&lt;p&gt;Before we can define custom rules we need to change our schema definition from the &lt;code&gt;Dry::Schema.JSON&lt;/code&gt; block to a response class that inherits from the &lt;code&gt;Dry::Validation::Contract&lt;/code&gt; class:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;Dry::Validation::Contract&lt;/code&gt; base class comes with a &lt;code&gt;rule&lt;/code&gt; method that we can use to raise custom validation failures if certain conditions match.&lt;/p&gt;

&lt;p&gt;Inside a rule we have access to a &lt;code&gt;values&lt;/code&gt; variable, which holds the value(s) of the configured property for that rule. As soon as we assign a failure message (by calling &lt;code&gt;key.failure("some message"))&lt;/code&gt; we will make the validation fail.&lt;/p&gt;

&lt;p&gt;Let’s say we want to validate the &lt;code&gt;done_at&lt;/code&gt; DateTime to always be after the &lt;code&gt;created_at&lt;/code&gt; DateTime, and each tag in &lt;code&gt;tags&lt;/code&gt; to only appear once. Then these might be our rule definitions:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;You can define whatever rules you can come up with to customize your validation contract and to adjust it to your needs. Dry-rb already provides the most often used data types, but you can also use fully customized ones as we saw in the examples above.&lt;/p&gt;

&lt;p&gt;Note that our first &lt;code&gt;ResponseSchema&lt;/code&gt; definition, where we made use of &lt;code&gt;Dry::Schema.JSON&lt;/code&gt;, gave us an instance of the schema class already. When using the &lt;code&gt;Dry::Validation::Contract&lt;/code&gt; we need to create an instance of our &lt;code&gt;ResponseSchema&lt;/code&gt; ourselves, before we can call its &lt;code&gt;call()&lt;/code&gt; method to validate our data.&lt;/p&gt;

&lt;p&gt;For more details and dry-validation features I encourage you to checkout the &lt;a href="https://dry-rb.org/gems/dry-validation" rel="noopener noreferrer"&gt;dry-validation documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Assertion Syntactical Sugar 🍬
&lt;/h2&gt;

&lt;p&gt;One issue with directly asserting if a schema validation succeeds is that you won’t see what went wrong if the validation failed. Dry-schema provides detailed error messages, so we can make use of these to debug our failing response validation.&lt;/p&gt;

&lt;p&gt;We can fix the visibility issue of failure messages by setting up a small test helper method:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;If the validation fails you will now be pointed to the erroneous properties and you will see a message about what went wrong. On top you will get the original JSON response, so you can spot the validation issue in no time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We saw that dry-rb gems, specifically the &lt;a href="https://dry-rb.org/gems/dry-schema" rel="noopener noreferrer"&gt;dry-schema&lt;/a&gt; and &lt;a href="https://dry-rb.org/gems/dry-validation" rel="noopener noreferrer"&gt;dry-validation&lt;/a&gt; gems provide us with some useful tools to make JSON response tests in Ruby more legible and maintainable.&lt;/p&gt;

&lt;p&gt;On top of making our JSON response tests more concise, the schemas we set up with this approach also allow us to have a quick overview about the structure and data types of our endpoint’s response.&lt;/p&gt;

&lt;p&gt;Schemas can be reused in other schemas and tests. They are easy to change and framework agnostic. So, although I used minitest assertion syntax in my code examples, with some small adjustments the shown approaches can also be applied if you use RSpec or any other Ruby testing library.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed this little journey into JSON response testing with dry-rb—thanks for reading and I’m curious to hear about your experiences with using dry-rb gems or similar approaches in your testing environment.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Happy coding, happy testing!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;small&gt;Credits: dryer icon in the header image by &lt;a href="https://www.flaticon.com/authors/photo3idea-studio" rel="noopener noreferrer"&gt;photo3idea_studio&lt;/a&gt; from &lt;a href="https://www.flaticon.com/" rel="noopener noreferrer"&gt;flaticon.com&lt;/a&gt; &lt;/small&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>json</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>Experiencing The Stroop Effect With a Ruby CLI</title>
      <dc:creator>Paul Götze</dc:creator>
      <pubDate>Wed, 24 Jun 2020 18:58:26 +0000</pubDate>
      <link>https://dev.to/paulgoetze/experiencing-the-stroop-effect-with-a-ruby-cli-1nnf</link>
      <guid>https://dev.to/paulgoetze/experiencing-the-stroop-effect-with-a-ruby-cli-1nnf</guid>
      <description>&lt;p&gt;You’ve probably seen these weird lists of color words before. Words whose text color does not match their content. Maybe you’ve even tried to read the text colors out loud quickly and have struggled to do so?&lt;/p&gt;

&lt;p&gt;These lists are part of psychological tests to demonstrate the so-called &lt;a href="https://en.wikipedia.org/wiki/Stroop_effect" rel="noopener noreferrer"&gt;Stroop effect&lt;/a&gt;. It is named after the American psychologist John Ridley Stroop, who first published the effect in English in 1935. &lt;/p&gt;

&lt;p&gt;Wikipedia tells us: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The Stroop effect is the delay in reaction time between congruent and incongruent stimuli."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But what does that actually mean? 🤔&lt;/p&gt;




&lt;p&gt;I built a tiny command line interface with Ruby – just because it was fun and so that you can experience the Stroop effect yourself. You can install and run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;stroop
stroop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Congruent? Incongruent?
&lt;/h1&gt;

&lt;p&gt;A Stroop tests consists of a small task, like reading color words. This task can be done with different kind of stimuli, namely: neutral, congruent, and incongruent.&lt;/p&gt;

&lt;p&gt;Let’s take a list of color words with a neutral stimulus, which means that all words are written in the same text color:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fazglsspg2ikyan4gcbe1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fazglsspg2ikyan4gcbe1.png" title="Neutral mode" alt="Color word box - neutral stimulus"&gt;&lt;/a&gt;&lt;/p&gt;
Neutral Stimulus – All color words are written in a neutral text color (black, or here gray)



&lt;p&gt;Reading all words out loud should not be hard to do.&lt;/p&gt;

&lt;p&gt;Next let’s take the same list of color words, but let’s print them in a color that matches the respective text. That’s a congruent stimulus:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fflew8kbqozksivva35zd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fflew8kbqozksivva35zd.png" alt="Color word box - congruent stimulus"&gt;&lt;/a&gt;&lt;/p&gt;
Congruent Stimulus – The word content matches the text color.



&lt;p&gt;If you read he words again, it might even be easier than with the neutral stimulus.&lt;/p&gt;

&lt;p&gt;Last, let’s print the same word list again, but this time we use another text color than the color that is represented in the text. That’s an incongruent stimulus:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F7j0hfha6i88b1cklog4s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F7j0hfha6i88b1cklog4s.png" alt="Color word box - incongruent stimulus"&gt;&lt;/a&gt;&lt;/p&gt;
Incongruent Stimulus – The word content and the text color are different.



&lt;p&gt;Reading the colors words might feel a bit slower now. However, the Stroop effect is much more apparent, if we slightly change our task:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instead of reading out loud the color words, try saying the text color for each word as fast as possible!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the incongruent stimulus you probably need much longer now and you might even make one or the other mistake. &lt;/p&gt;

&lt;p&gt;And that's the Stroop effect: the delay that appears if we speak the text color out loud for the incongruent word list compared to the congruent word list.&lt;/p&gt;

&lt;p&gt;Isn’t it fun – and really exhausting?&lt;/p&gt;




&lt;p&gt;🌱 The above word lists were generated using the &lt;a href="https://github.com/paulgoetze/stroop" rel="noopener noreferrer"&gt;stroop gem&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stroop neutral
stroop congruent
stroop incongruent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with the seed: 134414671674842647560860440639024210370&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(FYI: I found this tiny stroop CLI to also be perfect for creating colorful artsy-fartsy wallpapers &amp;amp; online profile backgrounds&lt;/em&gt; 😉&lt;em&gt;)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>sideprojects</category>
      <category>cli</category>
      <category>psychology</category>
    </item>
    <item>
      <title>Building a City Search with Elixir and Python</title>
      <dc:creator>Paul Götze</dc:creator>
      <pubDate>Thu, 28 May 2020 21:37:10 +0000</pubDate>
      <link>https://dev.to/paulgoetze/building-a-city-search-with-elixir-and-python-gf2</link>
      <guid>https://dev.to/paulgoetze/building-a-city-search-with-elixir-and-python-gf2</guid>
      <description>&lt;p&gt;The other day I was wondering whether there was an easy self-made local alternative to something like the &lt;a href="https://developers.google.com/places"&gt;Google Places API&lt;/a&gt;, that I could use in a Phoenix app. I wanted to search for a city and wanted to get back the city itself, its state, and the country.&lt;/p&gt;

&lt;p&gt;I found the free &lt;a href="http://dev.maxmind.com/geoip/geoip2/geolite2/#Downloads"&gt;GeoLite2&lt;/a&gt; city dataset, provided by Maxmind, which I could use to create a city search index.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;In case you directly want to dive into the programmatic materialisation of what I came up with, it is available &lt;a href="https://github.com/paulgoetze/elixir-python"&gt;on Github&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I did a quick search and stumbled upon the &lt;a href="https://github.com/elixir-search/searchex"&gt;searchex&lt;/a&gt; project by &lt;a href="https://github.com/andyl"&gt;@andyl&lt;/a&gt;. This actually looked like it was exactly what I was searching for. However, there is very little documentation yet. So, unfortunately, I couldn’t really figure out how to get it working.&lt;/p&gt;

&lt;p&gt;Then, while thinking about how to approach this, &lt;a href="https://whoosh.readthedocs.io"&gt;Whoosh&lt;/a&gt;, a Python package that I have used at work, came to my mind. Whoosh is a library for indexing text and searching the index. It is pretty easy to set up and delivers great search results with little effort.&lt;/p&gt;

&lt;p&gt;With this in my mind, I was wondering whether there was a way to call Python code from Elixir. After some further research and &lt;a href="https://medium.com/@Stephanbv/ruby-code-in-elixir-project-97614a9543d#.rp7o5vrpl"&gt;some&lt;/a&gt; &lt;a href="https://hackernoon.com/calling-python-from-elixir-erlport-vs-thrift-be75073b6536#.netzr6o72"&gt;articles&lt;/a&gt; later I found the Erlang library &lt;a href="http://erlport.org"&gt;erlport&lt;/a&gt;, which allows you to call Ruby and Python code from Elixir. &lt;/p&gt;

&lt;p&gt;There is also an Elixir wrapper for it, bearing the sounding name &lt;a href="https://github.com/fazibear/export"&gt;Export&lt;/a&gt;. You could also use erlport directly in Elixir, but Export gives you some convenient functions on top and a more Elixir-like feeling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up a New mix Project &amp;amp; Python virtualenv
&lt;/h2&gt;

&lt;p&gt;In order to get started with our custom city index, let’s set up a prototype mix project, called &lt;code&gt;elixir_python&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix new elixir_python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Head to the &lt;code&gt;mix.exs&lt;/code&gt; file and add the &lt;code&gt;export&lt;/code&gt; dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mix.exs&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:export&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 0.1.0"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install the dependencies with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix deps.get
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For setting up a Python environment you can use &lt;a href="https://virtualenv.pypa.io"&gt;virturalenv&lt;/a&gt; to create a local virtual environment. Also keep in mind to activate it after creating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;virtualenv &lt;span class="nt"&gt;-p&lt;/span&gt; python3 venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will use Whoosh, so we need a &lt;code&gt;requirements.txt&lt;/code&gt; next to our &lt;code&gt;mix.exs&lt;/code&gt; that defines the Python dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /requirements.txt

whoosh==2.7.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the requirements with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we need a directory where our Python code will live. Let’s create a &lt;code&gt;lib/python&lt;/code&gt; directory where we will put the *.py files later on. You can really put them wherever you want, you just have to link to the directory when using Export.&lt;/p&gt;

&lt;p&gt;In your &lt;code&gt;lib/python&lt;/code&gt; directory create a &lt;code&gt;geolite2.py&lt;/code&gt; file. This is where we will put the code for our city search index. Next, download the GeoLite2 CSV files from &lt;a href="http://dev.maxmind.com/geoip/geoip2/geolite2/#Downloads"&gt;dev.maxmind.com&lt;/a&gt; and put the English city locations in the &lt;code&gt;/lib/python/data&lt;/code&gt; directory. For our Python requirements we will also need a &lt;code&gt;requirements.txt&lt;/code&gt; file in our project’s root directory.&lt;/p&gt;

&lt;p&gt;Our Elixir code will live in &lt;code&gt;lib/elixir_python/geolite2.ex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The overall project structure should now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;└── elixir_python
    ├── config
    ├── lib
    │ ├── elixir_python
    │ │ └── geolite2.ex
    │ ├── python
    │ │ ├── data
    │ │ │ └── GeoLite2-City-Locations-en.csv
    │ │ ├── __init__.py
    │ │ └── geolite2.py
    │ └── elixir_python.ex
    ├── mix.exs
    ├── requirements.txt
    └── …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Python Part
&lt;/h2&gt;

&lt;p&gt;Our geolite2 Python module will have an API composed of two functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/python/geolite2.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# We will add code here in some minutes...
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# We will add some code here soon...
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one creates our search index using the GeoLite2 city CSV file. The second lets us search for cities, states or countries and will pass the results back to Elixir.&lt;/p&gt;

&lt;h3&gt;
  
  
  Indexing the City Data
&lt;/h3&gt;

&lt;p&gt;For each Whoosh index you can define a certain structure, its schema. The schema defines which data you want to store in the index and which fulltext – or content – you want to run the search on.&lt;/p&gt;

&lt;p&gt;Our city schema looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;whoosh.fields&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SchemaClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;whoosh.analysis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NgramWordAnalyzer&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CitySchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SchemaClass&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analyzer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NgramWordAnalyzer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;phrase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We want to store the city, the state, and the country. The &lt;code&gt;content&lt;/code&gt; field will hold the fulltext to search in, in our case it will be the joined city, state, and country name. This allows us to also search for cities, states or countries and provide multiple query terms to narrow down our results. &lt;/p&gt;

&lt;p&gt;We use an &lt;code&gt;NgramWordAnalyzer&lt;/code&gt; and set the &lt;code&gt;phrase&lt;/code&gt; argument to &lt;code&gt;False&lt;/code&gt; in order to save some space (see &lt;a href="https://whoosh.readthedocs.io/en/latest/recipes.html#itunes-style-search-as-you-type"&gt;this whoosh recipe&lt;/a&gt; for more details).&lt;/p&gt;

&lt;p&gt;Before creating the index let’s define our directory names and files we want to use along with some handy functions for building the absolute paths to these files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/python/geolite2.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;

&lt;span class="c1"&gt;# The base directory where out data lies, relative to this file
&lt;/span&gt;&lt;span class="n"&gt;DATA_BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'data'&lt;/span&gt;

&lt;span class="c1"&gt;# The actual city data file
&lt;/span&gt;&lt;span class="n"&gt;CITY_DATA_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'GeoLite2-City-Locations-en.csv'&lt;/span&gt;

&lt;span class="c1"&gt;# Our base directory where the index files are stored
&lt;/span&gt;&lt;span class="n"&gt;INDEX_BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'index'&lt;/span&gt;

&lt;span class="c1"&gt;# The name of our index
&lt;/span&gt;&lt;span class="n"&gt;CITY_INDEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'city'&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" Returns the absolute index path for the given index name """&lt;/span&gt;

    &lt;span class="n"&gt;index_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'{}_index'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_path&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INDEX_BASE_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;data_file_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" Returns the absolute path to the file with the given name """&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_path&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;DATA_BASE_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;current_path&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="s"&gt;""" Returns the absolute directory of this file """&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;abspath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;__file__&lt;/span&gt; &lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Armed with these helpers we can now go ahead and define the actual index creation function. We read the CSV file line by line, and create the schema from it. Some lines in the CSV do not represent cities but states or countries, so we skip these lines, unless there is a value in the city column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/python/geolite2.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;shutil&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;whoosh.index&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_in&lt;/span&gt;

&lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="s"&gt;""" Create search index files """&lt;/span&gt;

    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CITY_INDEX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;_recreate_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CitySchema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_file_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CITY_DATA_FILE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;csv_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;_add_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_recreate_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" Deletes and recreates the given path """&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rmtree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;makedirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_add_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" Writes the data to the index """&lt;/span&gt;

    &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'city_name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'subdivision_1_name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'country_name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the content (&lt;code&gt;"&amp;lt;city&amp;gt; &amp;lt;state&amp;gt; &amp;lt;country&amp;gt;"&lt;/code&gt;) is the actual text we analyse and put into the index. The rest of the schema properties is just stored data, which we can access again later on in our results and pass on to our Elixir app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Searching for Cities
&lt;/h3&gt;

&lt;p&gt;Let’s now implement the function for making a search request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/python/geolite2.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;whoosh.qparser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;QueryParser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;whoosh.query&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Prefix&lt;/span&gt;

&lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" Searches for the given query and returns `count` results """&lt;/span&gt;

    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;open_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CITY_INDEX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;searcher&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;searcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QueryParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;termclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;parsed_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;searcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'city'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                 &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'state'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                 &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'country'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we get the city index that we created with &lt;code&gt;create_index()&lt;/code&gt;. Then we build an instance of whoosh’s &lt;code&gt;QueryParser&lt;/code&gt; in order to parse our query using our city schema. We use the &lt;code&gt;termclass=Prefix&lt;/code&gt; here to only match documents that contain any term that starts with the given query text (see the &lt;a href="http://whoosh.readthedocs.io/en/latest/api/query.html#whoosh.query.Prefix"&gt;whoosh.query.Prefix&lt;/a&gt; docs). &lt;/p&gt;

&lt;p&gt;The parsed query is then passed to a searcher which finally runs the search and compiles the results for us. In order to keep it simple we collect the needed data in a list of lists. This will be the data we are going to receive from our Elixir function in a moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Elixir part
&lt;/h2&gt;

&lt;p&gt;Our Elixir API will look pretty much the same as the Python API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /lib/elixir_python/geolite2.ex&lt;/span&gt;

&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;ElixirPython&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;GeoLite2&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;create_index&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# We will add code here in some more minutes...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# We will add some code here later...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Calling Python
&lt;/h2&gt;

&lt;p&gt;To prepare for calling our Python functions from Elixir, add a &lt;code&gt;python_call/3&lt;/code&gt; function to the &lt;code&gt;ElixirPython&lt;/code&gt; module. It creates a Python instance for us and runs the Python code we provide it with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/elixir_python.ex&lt;/span&gt;

&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;ElixirPython&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Export&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Python&lt;/span&gt;

  &lt;span class="nv"&gt;@python_dir&lt;/span&gt; &lt;span class="s2"&gt;"lib/python"&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;-- this is the dir we created before&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;python_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Python&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;python_path:&lt;/span&gt; &lt;span class="no"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;@python_dir&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="no"&gt;Python&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We make use of Export’s Python module. &lt;code&gt;Python.start/1&lt;/code&gt; returns a tuple including a Python instance. In order to pick up our modules we pass the path to our Python directory as base path. &lt;code&gt;Python.call/4&lt;/code&gt; takes care of calling the given Python function from the respective module file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the City Index
&lt;/h3&gt;

&lt;p&gt;We use the &lt;code&gt;python_call/3&lt;/code&gt; function we just defined to run the &lt;code&gt;create_index&lt;/code&gt; function in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/elixir_python/geolite2.ex&lt;/span&gt;

&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;ElixirPython&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;GeoLite2&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="no"&gt;ElixirPython&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;python_call:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nv"&gt;@python_module&lt;/span&gt; &lt;span class="s2"&gt;"geolite2"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;create_index&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;python_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;@python_module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"create_index"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Searching for Cities
&lt;/h3&gt;

&lt;p&gt;To run a search query we use our &lt;code&gt;python_call/3&lt;/code&gt; function again to call the Python&lt;code&gt;search&lt;/code&gt; function we defined. The returned value is a list of lists holding the stored index data. We just loop over it and create Maps from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/elixir_python/geolite2.ex&lt;/span&gt;

&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;ElixirPython&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;GeoLite2&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="no"&gt;ElixirPython&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;python_call:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;python_call:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nv"&gt;@python_module&lt;/span&gt; &lt;span class="s2"&gt;"geolite2"&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;python_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;@python_module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;city:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;state:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;country:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Running a Search
&lt;/h2&gt;

&lt;p&gt;And we are done with our hunt for a city search index and we can use it now. Make sure you activated your Python virturalenv, then open up iex and give it a try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
iex &lt;span class="nt"&gt;-S&lt;/span&gt; mix
iex&lt;span class="o"&gt;(&lt;/span&gt;1&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; ElixirPython.GeoLite2.create_index&lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⌛️&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iex&lt;span class="o"&gt;(&lt;/span&gt;2&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; ElixirPython.GeoLite2.search&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Berlin"&lt;/span&gt;, 3&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;%&lt;span class="o"&gt;{&lt;/span&gt;city: &lt;span class="s2"&gt;"Berlin"&lt;/span&gt;, country: &lt;span class="s2"&gt;"Germany"&lt;/span&gt;, state: &lt;span class="s2"&gt;"Land Berlin"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
 %&lt;span class="o"&gt;{&lt;/span&gt;city: &lt;span class="s2"&gt;"Berlingen"&lt;/span&gt;, country: &lt;span class="s2"&gt;"Belgium"&lt;/span&gt;, state: &lt;span class="s2"&gt;"Flanders"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
 %&lt;span class="o"&gt;{&lt;/span&gt;city: &lt;span class="s2"&gt;"Falkenberg"&lt;/span&gt;, country: &lt;span class="s2"&gt;"Germany"&lt;/span&gt;, state: &lt;span class="s2"&gt;"Land Berlin"&lt;/span&gt;&lt;span class="o"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yay. It works!&lt;/p&gt;

&lt;p&gt;Let’s try another one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iex&lt;span class="o"&gt;(&lt;/span&gt;3&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; ElixirPython.GeoLite2.search&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"San José"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hm, why is that? We couldn’t find any results, although San José is definitely in the index. It’s because our system does not normalise special characters and accents yet. Let’s do this in a final next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Special Characters
&lt;/h2&gt;

&lt;p&gt;On the Elixir side this is easy to do. There is an Erlang lib called &lt;a href="https://github.com/processone/iconv"&gt;iconv&lt;/a&gt;. Let’s just add it to our &lt;code&gt;mix.exs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mix.exs&lt;/span&gt;

&lt;span class="c1"&gt;#...&lt;/span&gt;
&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:export&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 0.1.0"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:iconv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.0"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install the dependency with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix deps.get
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s now preprocess the query before we pass it to our Python function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/elixir_python/geolite2.ex&lt;/span&gt;

&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;ElixirPython&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;GeoLite2&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clean_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;clean_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="ss"&gt;:iconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ascii//translit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we rerun our query now we get the wanted city in the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iex&lt;span class="o"&gt;(&lt;/span&gt;4&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; ElixirPython.GeoLite2.search&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"San José"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;%&lt;span class="o"&gt;{&lt;/span&gt;city: &lt;span class="s2"&gt;"San José"&lt;/span&gt;, country: &lt;span class="s2"&gt;"Costa Rica"&lt;/span&gt;, state: &lt;span class="s2"&gt;"Provincia de San Jose"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We still have some problems with e.g. German cities, like Görlitz, that use Umlauts, So let’s transform them to their ASCII counterparts before creating the index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/python/geolite2.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;unicodedata&lt;/span&gt;

&lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_add_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" Writes the data to the index """&lt;/span&gt;

    &lt;span class="c1"&gt;# ...
&lt;/span&gt;
    &lt;span class="c1"&gt;# clean up the content that goes to the index by using _cleanup_text():
&lt;/span&gt;    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cleanup_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_cleanup_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" Removes accents and replaces umlauts """&lt;/span&gt;

    &lt;span class="n"&gt;replaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'ä'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'ae'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'ö'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'oe'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'ü'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'ue'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'Ä'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Ae'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'Ö'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Oe'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'Ü'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Ue'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'ß'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'ss'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;replaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'NFKD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;combining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ascii'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'ignore'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ascii'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we recreate our index and run a &lt;code&gt;Görlitz&lt;/code&gt; query now, we will get some fitting results.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;We managed to build a small local fulltext index for a city search without too much effort. Our system returns great search results and we allowed to search for cities with or without using special characters.&lt;/p&gt;

&lt;p&gt;All in all, it does not scale very well, though. I tried to build another, more sophisticated city index including about 4.4 M cities and villages world-wide and the location coordinates (latitude and longitude) and time zone for each place. &lt;/p&gt;

&lt;p&gt;If you are interested you can find the script for combining the city data and location data in &lt;a href="https://gist.github.com/paulgoetze/3fea5dfb2b757a46aec25d5bcfd1359d"&gt;this gist&lt;/a&gt;. It took quite some time to build the index (about 40 minutes on my laptop) and resulted in an index file of 1.3 GB size (compared to ~29 MB for the GeoLite2 index). &lt;/p&gt;

&lt;p&gt;Although it also worked well and you will get fitting search results, it takes about 10 seconds to finish a single request. This approach would need some additional caching and further optimisation in order to be useful in any kind of way.&lt;/p&gt;

&lt;p&gt;So, eventually, I ended up using the Google Places API anyway 😉. But, hey: “Wieder was gelernt.”&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>python</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Adoptoposs.org – Find Co-maintainers For Your Open Source Project</title>
      <dc:creator>Paul Götze</dc:creator>
      <pubDate>Mon, 18 May 2020 23:12:26 +0000</pubDate>
      <link>https://dev.to/paulgoetze/announcing-adoptoposs-org-255f</link>
      <guid>https://dev.to/paulgoetze/announcing-adoptoposs-org-255f</guid>
      <description>&lt;p&gt;I am happy to announce &lt;a href="https://adoptoposs.org"&gt;Adoptoposs.org&lt;/a&gt;, an open source app that connects open source software maintainers with people who want to help keep projects and maintainers healthy in the long term.&lt;/p&gt;

&lt;p&gt;Adoptoposs offers a platform to put your open source projects up for adoption and make initial contact with potential co-maintainers.&lt;/p&gt;

&lt;p&gt;The source code is available on GitHub: &lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/adoptoposs"&gt;
        adoptoposs
      &lt;/a&gt; / &lt;a href="https://github.com/adoptoposs/adoptoposs"&gt;
        adoptoposs
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Finding co-maintainers for your open source software project.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;em&gt;If you’d like to learn more about the reasons why I built Adoptoposs, the problem of unmaintained open source software, and what might be part of the solution, then read on.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Open source software (OSS) incontrovertibly runs our world. Enterprise open source is considered to continue to grow in 2020. Most of the internet is running on open source software and millions of developers build and maintain hundreds of thousands of open source software packages in more than 250 programming languages these days.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Problem of Unmaintained OSS Projects
&lt;/h1&gt;

&lt;p&gt;Whereas most developers use and contribute to open source software at work, the maintenance of a great amount of OSS projects is done by volunteers in their spare time.&lt;/p&gt;

&lt;p&gt;Many projects grow out of a personal side-project into popular and widely used libraries. With a lot of attraction, more issues and pull requests are opened by the community. A maintainer’s growing responsibility is now to orchestrate incoming requests and changes, and steer the whole project.&lt;/p&gt;

&lt;p&gt;There are loads of stories about the challenges and struggles of OSS maintainers. They include emotionally exhausting community management and the great effort that has to be put into triaging of issues and pull requests.&lt;/p&gt;

&lt;p&gt;When hearing these stories of mostly voluntary efforts, it’s no surprise that maintainers feel overwhelmed by the amount of work and turn their back on their projects.&lt;/p&gt;

&lt;p&gt;Reasons for dedicating less time to or even leaving an open source project are many. Maintainers leave their company or lose interest. Changes in their personal lives give them less time to take care of the project or they stop their activities in the open source scene entirely because of burnout or illness. In the worst case, they have passed away.&lt;/p&gt;

&lt;p&gt;All these cases are leaving behind projects that often have no one but the original author with administration rights or access to the publishing accounts.&lt;/p&gt;

&lt;p&gt;Bug reports stack up, pull requests don’t get merged, compatibility issues with the surrounding ecosystem emerge. Then, at some point, someone in the community steps up and opens the infamous &lt;a href="https://github.com/search?q=Is+this+project+abandoned%3F&amp;amp;type=Issues"&gt;“Is this project abandoned?”&lt;/a&gt; Issue.&lt;/p&gt;

&lt;p&gt;Eventually, the project might be forked and fixes are being made by new volunteers from the community. New versions of the project are published under a new name because of lacking publishing access.&lt;/p&gt;

&lt;p&gt;All this leads to confusion about the state of the project and whether it can and should still be used and considered as a stable piece of software.&lt;/p&gt;




&lt;h1&gt;
  
  
  So, What’s the Solution?
&lt;/h1&gt;

&lt;p&gt;Simply put, each and every open source project of considerable popularity should have a &lt;em&gt;team of maintainers&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;What does “considerable popularity” mean? When do you know your project is an important dependency for others? You will probably know really soon by its download rates and the popularity of its direct dependents.&lt;/p&gt;

&lt;p&gt;However, if it already gained popularity and you are still operating the project on a single-maintainer policy then maintenance issues are likely to loom on the horizon sooner rather than later.&lt;/p&gt;

&lt;p&gt;Having a team of multiple maintainers, however, is likely to result in a better-maintained project for several good reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The workload can be split up between all maintainers
&lt;/h3&gt;

&lt;p&gt;Every one of us has different preferences on what to work on, different interests, and skills. And probably none of us is highly skilled and interested in working on all the tasks that open source maintenance demands.&lt;/p&gt;

&lt;p&gt;Thus, it makes sense to build a team and split up the entire workload to let every maintainer work on the part they like and can do best.&lt;/p&gt;

&lt;p&gt;Less workload will also mean less risk to burn out and will allow maintainers to better look after themselves. Just like what teams in a company are built for.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It’s easier to make decisions as a team
&lt;/h3&gt;

&lt;p&gt;Not only does it take the pressure off every maintainer when decisions have to be made. It also potentially leads to more stability and more informed decisions.&lt;/p&gt;

&lt;p&gt;Different points of view are taken into account by default. As the old saying goes: “A fresh pair of eyes prevents many bugs’ rise.”&lt;/p&gt;

&lt;h3&gt;
  
  
  3. There will be no single point of failure
&lt;/h3&gt;

&lt;p&gt;In our daily lives of software development, we aim at reaching availability and fault tolerance by introducing redundancy. Our clusters consist of redundant servers, we use Git to decentralize repositories, and we (hopefully) create backups of our personal documents.&lt;/p&gt;

&lt;p&gt;All of these measures try to prevent a single point of failure. By working in a team of co-maintainers, where multiple people have full access to the project, this single point of failure can be extinguished.&lt;/p&gt;

&lt;p&gt;Communication with the community can happen, pull requests can be merged, releases can be published, and access can be granted even if one or more co-maintainers are not available at times.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. It is possible to step down as a maintainer
&lt;/h3&gt;

&lt;p&gt;It’s important to allow maintainers to leave the project when they decide to do so. If you leave your projects as a solo maintainer then it is abandoned. Unmaintained at a moment’s notice.&lt;/p&gt;

&lt;p&gt;The community has to put extra effort into keeping it alive or bringing it to life again. If nobody has administration and publishing rights, this process will even get harder.&lt;/p&gt;

&lt;p&gt;Having a team of co-maintainers allows you to think about how to deal with someone leaving the project and how to onboard new co-maintainers.&lt;/p&gt;

&lt;p&gt;When having multiple maintainers in place, if someone wants to leave the project, they can just do that — without inevitably putting the project into an unmaintained state.&lt;/p&gt;




&lt;h1&gt;
  
  
  What Can I Do?
&lt;/h1&gt;

&lt;p&gt;As a maintainer of open source software, you should remove the single point of failure of your project by building a team of co-maintainers. Granting multiple people full access to the project will ensure that there is always more than a single person who can administrate the project.&lt;/p&gt;

&lt;p&gt;It doesn’t stop at giving access to the repository or GitHub organization, though. You should also give your co-maintainers push access to package registries and all other related accounts in the publishing process of your project.&lt;/p&gt;

&lt;p&gt;Co-maintainers do not necessarily need to be people who are working a lot on the project. They might just be the one in charge in case of an emergency. This can also involve giving new maintainers access when former maintainers want to step down. Similar to a custodian.&lt;/p&gt;

&lt;p&gt;If you want to leave a project, you should communicate that clearly and inform the involved people and the community. Try to educate people about your reasons and how they can help with maintaining the project.&lt;/p&gt;

&lt;p&gt;Don’t forget to share and document knowledge while working on the project, so that if someone leaves, there is little to no unknowns for the rest of the team.&lt;/p&gt;

&lt;p&gt;You can use community-driven projects that help with maintaining open source software, such as the wonderful &lt;a href="https://codetriage.com/"&gt;CodeTriage&lt;/a&gt; project, collaborative communities like &lt;a href="https://jazzband.co/"&gt;Jazzband&lt;/a&gt; (read an interview on opensource.com), and programs like &lt;a href="https://github.com/adopted-ember-addons/program-guidelines"&gt;Adopted Ember Addons&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And now there’s &lt;a href="https://adoptoposs.org/"&gt;Adoptoposs&lt;/a&gt;, too, hopefully helping with keeping your projects and yourself healthy while maintaining open source software.&lt;br&gt;
Check it out, and happy maintaining.&lt;/p&gt;

&lt;p&gt;By the way — Adoptoposs is always happy to hear about your feedback. It’s built with &lt;a href="https://elixir-lang.org/"&gt;Elixir&lt;/a&gt;, &lt;a href="https://github.com/phoenixframework/phoenix_live_view"&gt;Phoenix LiveView&lt;/a&gt;, and &lt;a href="https://tailwindcss.com/"&gt;Tailwind CSS&lt;/a&gt;, and is currently hosted on Heroku. If you want to help, don’t hesitate to check our open issues and discuss new features with us. Issues can be reported in the &lt;a href="https://github.com/adoptoposs/adoptoposs/issues"&gt;GitHub repository&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--K-gn4Ffv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://adoptoposs.org/images/adoptoposs-logo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--K-gn4Ffv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://adoptoposs.org/images/adoptoposs-logo.png" alt="The Adoptoposs" width="204" height="80"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;Here are some insightful links and references that I came across when doing research for building Adoptoposs and writing this post.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nayafia"&gt;
        nayafia
      &lt;/a&gt; / &lt;a href="https://github.com/nayafia/awesome-maintainers"&gt;
        awesome-maintainers
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Talks, blog posts, and interviews about the experience of being an open source maintainer
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.wired.com/story/giving-open-source-projects-life-after-a-developers-death"&gt;Giving Open-Source Projects Life After a Developer's Death&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opensource.com/article/17/4/how-to-leave-open-source-project"&gt;How to deal with leaving an open source project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://changelog.com/rfc"&gt;Request For Commits Podcast with Nadia Eghbal and Mikeal Rogers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.redhat.com/en/enterprise-open-source-report/2020"&gt;The State of Enterprise Open Source: A Red Hat Report&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opensourcesurvey.org/2017/"&gt;https://opensourcesurvey.org/2017/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opensource.guide/best-practices/"&gt;https://opensource.guide/best-practices/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Situations where projects could not be maintained anymore
&lt;/h3&gt;

&lt;p&gt;A widely used Python package, where a former maintainer left the company and was off-boarded from the team, had no access anymore, and could not merge existing PRs with bugfixes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/seatgeek/fuzzywuzzy/pull/243"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        Fix for Python 3.7
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#243&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/hb-alexbotello"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s--FhMZxTAo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://avatars2.githubusercontent.com/u/49160939%3Fv%3D4" alt="hb-alexbotello avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/hb-alexbotello"&gt;hb-alexbotello&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/seatgeek/fuzzywuzzy/pull/243"&gt;&lt;time&gt;Jul 26, 2019&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;p&gt;Fixes #233&lt;/p&gt;
&lt;p&gt;According to &lt;a href="https://www.python.org/dev/peps/pep-0479/#examples-of-breakage" rel="nofollow"&gt;PEP 479&lt;/a&gt;, if raise StopIteration occurs directly in a generator, simply replace it with return.&lt;/p&gt;
&lt;p&gt;This is both backwards and forwards compatible code.&lt;/p&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/seatgeek/fuzzywuzzy/pull/243"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;Another Python package, flask-security, that was forked multiple times and is now maintained by Chris Wagner and lives on as Flask-Security-Too:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Flask-Middleware"&gt;
        Flask-Middleware
      &lt;/a&gt; / &lt;a href="https://github.com/Flask-Middleware/flask-security"&gt;
        flask-security
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Quick and simple security for Flask applications
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="rst"&gt;
&lt;h1&gt;
Flask-Security&lt;/h1&gt;
&lt;a href="https://github.com/Flask-Middleware/flask-security"&gt;&lt;img alt="https://github.com/Flask-Middleware/flask-security/workflows/tests/badge.svg?branch=master&amp;amp;event=push" src="https://res.cloudinary.com/practicaldev/image/fetch/s--q1VaxBEY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/Flask-Middleware/flask-security/workflows/tests/badge.svg%3Fbranch%3Dmaster%26event%3Dpush"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/Flask-Middleware/flask-security" rel="nofollow"&gt;&lt;img alt="Coverage!" src="https://camo.githubusercontent.com/87e943aa0c1f2efd9b064f4bd08b47df67d52b70b2983d7159e786a193fd91d8/68747470733a2f2f636f6465636f762e696f2f67682f466c61736b2d4d6964646c65776172652f666c61736b2d73656375726974792f6272616e63682f6d61737465722f67726170682f62616467652e7376673f746f6b656e3d5530324d55514a37424d"&gt;&lt;/a&gt;
&lt;a href="https://github.com/Flask-Middleware/flask-security/releases"&gt;&lt;img src="https://camo.githubusercontent.com/f43b6e3961db50ee420efb4aed96d97914da9f9aa4b2b27a26d693356313bfb6/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f7461672f466c61736b2d4d6964646c65776172652f666c61736b2d73656375726974792e737667"&gt;
&lt;/a&gt;
&lt;a href="https://pypi.python.org/pypi/flask-security-too" rel="nofollow"&gt;&lt;img alt="Downloads" src="https://camo.githubusercontent.com/79ef3d1cfec7b594266d3b3dafea8d71b9fea5373109a04507029957d83815eb/68747470733a2f2f696d672e736869656c64732e696f2f707970692f646d2f666c61736b2d73656375726974792d746f6f2e737667"&gt;
&lt;/a&gt;
&lt;a href="https://github.com/Flask-Middleware/flask-security/blob/master/LICENSE"&gt;&lt;img alt="License" src="https://camo.githubusercontent.com/23930c48e4aead585f3379d86f30866081027e6fcd88a727b3c9bdc0943ab670/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f466c61736b2d4d6964646c65776172652f666c61736b2d73656375726974792e737667"&gt;
&lt;/a&gt;
&lt;a href="https://flask-security-too.readthedocs.io/en/latest/?badge=latest" rel="nofollow"&gt;&lt;img alt="Documentation Status" src="https://camo.githubusercontent.com/848253ce2aa3fdd6666c5d4eeac7748779a48eb5c19844cadc2f7f1d9c2bfd89/68747470733a2f2f72656164746865646f63732e6f72672f70726f6a656374732f666c61736b2d73656375726974792d746f6f2f62616467652f3f76657273696f6e3d6c6174657374"&gt;&lt;/a&gt;
&lt;a href="https://github.com/python/black"&gt;&lt;img src="https://camo.githubusercontent.com/d91ed7ac7abbd5a6102cbe988dd8e9ac21bde0a73d97be7603b891ad08ce3479/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f64652532307374796c652d626c61636b2d3030303030302e737667"&gt;
&lt;/a&gt;
&lt;a href="https://github.com/pre-commit/pre-commit"&gt;&lt;img alt="pre-commit" src="https://camo.githubusercontent.com/aca250e5eab7c4ba30771c05b360513abb591643c5f2f2f6997c8fb9bc98197f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7072652d2d636f6d6d69742d656e61626c65642d627269676874677265656e3f6c6f676f3d7072652d636f6d6d6974266c6f676f436f6c6f723d7768697465"&gt;&lt;/a&gt;
&lt;p&gt;Quickly add security features to your Flask application.&lt;/p&gt;
&lt;h2&gt;
Notes on this repo&lt;/h2&gt;
&lt;p&gt;This is a independently maintained version of Flask-Security based on the 3.0.0
version of the &lt;a href="https://github.com/mattupstate/flask-security"&gt;Original&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
Goals&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Regain momentum for this critical piece of the Flask eco-system. To that end the
the plan is to put out small, frequent releases starting with pulling the simplest
and most obvious changes that have already been vetted in the upstream version, as
well as other pull requests. This was completed with the June 29 2019 3.2.0 release.&lt;/li&gt;
&lt;li&gt;Continue work to get Flask-Security to be usable from Single Page Applications
such as those built with Vue and Angular, that have no html forms. This is true as of the 3.3.0
release.&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://github.com/OWASP/ASVS"&gt;OWASP&lt;/a&gt; to guide best practice and default configurations.&lt;/li&gt;
&lt;li&gt;Be more opinionated and 'batteries' included by reducing reliance on abandoned projects and
bundling in support for common use cases.&lt;/li&gt;
&lt;li&gt;Follow…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Flask-Middleware/flask-security"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/mattupstate/flask-security/issues/854"&gt;https://github.com/mattupstate/flask-security/issues/854&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/italomaia/flask-empty/issues/35"&gt;https://github.com/italomaia/flask-empty/issues/35&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Ryan Bates, the creator of railscasts.com, paused software development because of burnout. His CanCan Ruby gem lives on as &lt;a href="https://github.com/CanCanCommunity/cancancan"&gt;CanCanCan&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/ryanb/cancan/issues/994"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        NOTICE: cancan is moving!
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#994&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/codyolsen"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s--vEGLmkBF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://avatars0.githubusercontent.com/u/3844315%3Fv%3D4" alt="codyolsen avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/codyolsen"&gt;codyolsen&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/ryanb/cancan/issues/994"&gt;&lt;time&gt;Feb 21, 2014&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;blockquote&gt;
&lt;p&gt;"parting is such sweet sorrow"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As discussed for the last month in the comment thread of pull &lt;a href="https://github.com/ryanb/cancan/pull/989"&gt;https://github.com/ryanb/cancan/pull/989&lt;/a&gt; it is time to move cancan forward before it fizzles. While the community support behind this gem is high, and pull requests have been added daily, the last merge into the official gem was in September of 2013. We both love and support the work that &lt;a class="mentioned-user" href="https://dev.to/ryanb"&gt;@ryanb&lt;/a&gt; has given to this gem, and in hopes to keep his legacy alive we have the following solution:&lt;/p&gt;
&lt;h4&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Enter &lt;code&gt;cancancan&lt;/code&gt;
&lt;/h4&gt;
&lt;p&gt;This will be the new gem repository going forward. Please put issues and pull requests in the new repo, as we will now be able to merge fixes into the main gem. Thanks to @bryanrite, as he has spearheaded the effort for the new repo.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://goo.gl/J78fDt" rel="nofollow"&gt;https://github.com/cancancommunity/cancancan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://goo.gl/AtrRsz" rel="nofollow"&gt;https://rubygems.org/gems/cancancan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For more details on the transition, please look at the &lt;a href="https://github.com/bryanrite/cancancan/blob/master/README.rdoc#mission"&gt;readme&lt;/a&gt; on the new repo and the &lt;a href="https://github.com/ryanb/cancan/pull/989"&gt;comment thread&lt;/a&gt;.&lt;/p&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ryanb/cancan/issues/994"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;Michael Kessler (netzpirat), the maintainer of the haml-coffee Ruby gem, passed away:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/netzpirat/haml-coffee/issues/97"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        Attention: @netzpirat passed away in April '14
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#97&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/janv"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s--dLFh7MfU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://avatars0.githubusercontent.com/u/25069%3Fv%3D4" alt="janv avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/janv"&gt;janv&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/netzpirat/haml-coffee/issues/97"&gt;&lt;time&gt;May 05, 2014&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;p&gt;I, together with &lt;a class="mentioned-user" href="https://dev.to/sebastiandeutsch"&gt;@sebastiandeutsch&lt;/a&gt; were the first committers on the project, before handing it over to Michael (@netzpirat).&lt;/p&gt;

&lt;p&gt;I have just learned that Michael has &lt;a href="https://groups.google.com/forum/?utm_source=rubyweekly&amp;amp;utm_medium=email#!msg/guard-dev/2Td0QTvTIsE/cegvVofIJ8AJ" rel="nofollow"&gt;passed away in the beginning of April&lt;/a&gt;. I'm putting up this issue to bring this to the attention of HAML-Coffee users.&lt;/p&gt;

&lt;p&gt;This also means that haml-coffee needs a new maintainer. Unfortunately my free time doesn't allow for maintenance of this project in the extent Michael worked on HAML-Coffee.&lt;/p&gt;

&lt;p&gt;:(&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;/div&amp;gt;
&amp;lt;div class="gh-btn-container"&amp;gt;&amp;lt;a class="gh-btn" href="https://github.com/netzpirat/haml-coffee/issues/97"&amp;gt;View on GitHub&amp;lt;/a&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;
 
&lt;/div&gt;

</description>
      <category>showdev</category>
      <category>news</category>
      <category>opensource</category>
      <category>github</category>
    </item>
  </channel>
</rss>
