<?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>Forem: Fabio Manganiello</title>
    <description>The latest articles on Forem by Fabio Manganiello (@blacklight).</description>
    <link>https://forem.com/blacklight</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%2F299449%2F84d6f992-880c-4234-bfe2-35ffd8d3f69f.jpeg</url>
      <title>Forem: Fabio Manganiello</title>
      <link>https://forem.com/blacklight</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/blacklight"/>
    <language>en</language>
    <item>
      <title>Correlation without tears</title>
      <dc:creator>Fabio Manganiello</dc:creator>
      <pubDate>Fri, 11 Feb 2022 12:09:52 +0000</pubDate>
      <link>https://forem.com/blacklight/correlation-without-tears-2e1o</link>
      <guid>https://forem.com/blacklight/correlation-without-tears-2e1o</guid>
      <description>&lt;p&gt;Assessing the degree of correlation between two numeric series is a notoriously challenging task in every scientific discipline, as well as a crucial aspect of every scientific research (ever wondered if there's a correlation between the usage of Internet Explorer and the murder rate?).&lt;/p&gt;

&lt;p&gt;Different coefficients have been proposed over the years for different series with different properties (most notably, Pearson's, Spearman's and Kendall's coefficients), and the quest for a "universal" correlation coefficient has often been unproductive.&lt;/p&gt;

&lt;p&gt;It took me a while to digest the math behind the new paper from Sourav Chatterjee (&lt;a href="https://arxiv.org/abs/1909.10140"&gt;https://arxiv.org/abs/1909.10140&lt;/a&gt;), but once I modelled the proposed coefficient into Python code I was surprised by how well it performed on arbitrary numeric series (not necessarily monotonic) compared to most of the coefficients out there. And it's also very easy to calculate compared to other coefficients.&lt;/p&gt;

&lt;p&gt;So I've put together a &lt;a href="https://gist.github.com/bf5da64cca77916ee970f0432622adc0"&gt;Gist notebook&lt;/a&gt; that shows how the new coefficient works on some simple data with increasing levels of noise.&lt;/p&gt;

&lt;p&gt;Feel free to reuse the code if you need it!&lt;/p&gt;

</description>
      <category>python</category>
      <category>correlation</category>
      <category>statistics</category>
    </item>
    <item>
      <title>[Trivia] What physical address is mentioned the most in all the software ever written?</title>
      <dc:creator>Fabio Manganiello</dc:creator>
      <pubDate>Fri, 28 Jan 2022 21:27:26 +0000</pubDate>
      <link>https://forem.com/blacklight/trivia-what-physical-address-is-mentioned-the-most-in-all-the-software-ever-written-4phf</link>
      <guid>https://forem.com/blacklight/trivia-what-physical-address-is-mentioned-the-most-in-all-the-software-ever-written-4phf</guid>
      <description>&lt;p&gt;While contributing to some open-source repo the other day, I diligently copied that copyright license boilerplate that every good developer adds to their code without ever reading. For the first time in two decades, I decided to actually read it, and the last lines particularly triggered my attention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
* 02111-1307, USA.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first obvious question is: does the FSF today still process paper letters coming from people all over the world who want a paper copy of a license that can be found on the web within 2 ms? I've been honestly tempted by the idea of sending a physical letter to the FSF and request a hard copy of the GPL 3 license, just to frame it in my room (fax could also be an interesting option, who know if they provide the feature).&lt;/p&gt;

&lt;p&gt;The second obvious question is: just how common is this address in today's code? So &lt;a href="https://github.com/search?q=59+Temple+Place"&gt;I asked Github&lt;/a&gt;, and got back an astonishing 131M results.&lt;/p&gt;

&lt;p&gt;Do you know any other address that may be referenced more often in all the software ever written?&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Web 3.0 and the undeliverable promise of decentralization</title>
      <dc:creator>Fabio Manganiello</dc:creator>
      <pubDate>Mon, 17 Jan 2022 09:14:11 +0000</pubDate>
      <link>https://forem.com/blacklight/web-30-and-the-undeliverable-promise-of-decentralization-9ca</link>
      <guid>https://forem.com/blacklight/web-30-and-the-undeliverable-promise-of-decentralization-9ca</guid>
      <description>&lt;h2&gt;
  
  
  What was the Web 3.0 supposed to mean again?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Semantic Web
&lt;/h3&gt;

&lt;p&gt;The definition of "Web 3.0" has assumed different incarnations over the past few years, depending on whom you asked and when. We initially thought it would have been some kind of &lt;em&gt;semantic web&lt;/em&gt;, with shared ontologies and taxonomies that described all the domains relevant to the published content. The Internet, according to this vision, will be a collective semantic engine driven by a shared language to describe entities and the way they connect and interact with one another. It will be easy for a machine to extract the "meaning" and the main entities of another page, so the Internet will be a network of &lt;em&gt;connected concepts&lt;/em&gt; rather than a network of &lt;em&gt;connected pages&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Looking at things in 2022, we can arguably say that this vision hasn't (yet?) been realized. Despite being envisioned for at least the &lt;a href="http://bluehawk.monmouth.edu/~rscherl/Classes/CS598/slides13n4.pdf"&gt;past 20 years&lt;/a&gt;, today approximately &lt;a href="http://iswc2013.semanticweb.org/content/keynote-ramanathan-v-guha.html"&gt;1-2% of the Internet domains&lt;/a&gt; contain or support the semantic markup extensions proposed by &lt;a href="https://en.wikipedia.org/wiki/Resource_Description_Framework"&gt;RDF&lt;/a&gt; / &lt;a href="https://en.wikipedia.org/wiki/Web_Ontology_Language"&gt;OWL&lt;/a&gt;. I still believe that there's plenty of untapped potential in that area but, since there hasn't been a compelling commercial use-case to invest tons of resources in building a shared &lt;em&gt;Internet of meaning&lt;/em&gt; instead of an &lt;em&gt;Internet of links&lt;/em&gt;, the discussion has gotten stranded between academia and W3C standards (see also &lt;a href="https://signal.org/blog/the-ecosystem-is-moving/"&gt;the curse of protocols and shared standards&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Web-of-Things
&lt;/h3&gt;

&lt;p&gt;More recently, &lt;a href="https://www.uxbooth.com/articles/web-2-0-web-3-0-and-the-internet-of-things/"&gt;we envisioned the Web 3.0 as the &lt;em&gt;Internet of Things&lt;/em&gt;&lt;/a&gt;.  According to this vision, IPv6 should have taken over within a few years, every device would have been directly connected to the Internet with no need of workarounds such as &lt;a href="https://en.wikipedia.org/wiki/Network_address_translation"&gt;NAT&lt;/a&gt; (IPv6 provides enough addresses for each of the atoms on the planet), all the devices will ideally be talking directly to one another over some extension of HTTP.&lt;/p&gt;

&lt;p&gt;Looking at things in 2022, we can say that this vision hasn't (yet) become a reality either. Many years down the line, &lt;a href="https://www.google.com/intl/en/ipv6/statistics.html"&gt;only one third of the traffic on the Internet is IPv6&lt;/a&gt;. In countries like Italy and Spain, the adoption rate is less than 4-5%. So NAT-ting is still a thing, with the difference that it has now been largely delegated to a few centralized networks (mainly Google, Apple and Amazon and their ecosystems): so much for the dream of a decentralized Internet of things. And the situation on the protocols side isn't that rosy either. The problem of fragmentation in the IoT ecosystem is &lt;a href="https://internetofthingsagenda.techtarget.com/blog/IoT-Agenda/The-inevitable-fragmentation-of-IoT"&gt;well documented&lt;/a&gt;.  The main actors in this space have built their own ecosystems, walled gardens and protocols.  &lt;a href="https://buildwithmatter.com/"&gt;The Matter standard&lt;/a&gt; should solve some of these problems, but after a lot of hype &lt;a href="https://github.com/project-chip/connectedhomeip#readme"&gt;there isn't much usable code on it yet, nor usable specifications&lt;/a&gt; (2022 should be the year of Matter though, that's what everybody says). Regardless of how successful the Matter project will be in solving the fragmentation that prevents the Internet of things from being an actual thing, it's sobering to look back at how far we've come from the original vision. We initially envisioned a decentralized web of devices, each with its own address, all talking the same protocols, all adhering to the same standards. We have instead ended up with an IoT run over a few very centralized platforms, with limited capabilities when it comes to talking to one another, and the most likely to solution to this problem will be a new standard which hasn't been established by academic experts, ISO or IEEE, but by the same large companies that run the current oligopoly. Again, so much for the dream of a decentralized Internet of things.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crypto to the rescue?
&lt;/h3&gt;

&lt;p&gt;Coming to the definition of Web 3.0 most popular in the last couple of years, we've got the Internet-over-Blockchain, a.k.a. crypto-web, a.k.a. &lt;a href="https://ethereum.org/en/dapps/"&gt;dApps&lt;/a&gt;. The idea is pretty straightforward: if Blockchain can store financial transactions in a decentralized way, and it can also run some code that transparently manipulates its data in the form of smart contracts, then we've got the foundations to build both data and code in a decentralized way.&lt;/p&gt;

&lt;p&gt;To understand how we've got here, let's take a step back and let's ask ourselves a simple question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problems is the Web 3.0 actually trying to solve?
&lt;/h2&gt;

&lt;p&gt;What is wrong with the Web 2.0 that actually wants us to move towards a Web 3.0, with all the costs involved in changing the way we organize software, networks, companies and whole economies around them?&lt;/p&gt;

&lt;p&gt;Even if they envision different futures, the Semantic Web, the Web-of-Things and the Crypto-Web have several elements in common. And the common trait to all those elements can be summarized by a single word: &lt;em&gt;decentralization&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So why is the Web 3.0, in all of its incarnations, so much into decentralization?&lt;/p&gt;

&lt;p&gt;An obvious part to the answer is, of course, that the Web 2.0 has become too much about centralization (of data, code, services, and infrastructure) in the hands of a few corporations.&lt;/p&gt;

&lt;p&gt;The other part of the answer to the question is the fact that software engineering, in all of its forms, is a science that studies a pendulum that swings back and forth between centralization and decentralization (the most cynical would argue that this is the reason why information engineers will never be jobless). This common metaphor, however, provides only a low-resolution picture of reality. I believe that the information technology has a strong center of mass that pulls towards centralization. When the system has enough energy (for various reason that we'll investigate shortly) it flips towards decentralization, just to flip back to a centralized approach when the cost of running a system with an increasingly high number of moving parts becomes too high. At that point, most of the people are happy to step back and accept somebody providing them with infrastructure/platform/software-as-a-service®, so they can just focus on whatever their core business is. At that point, the costs and risks of running the technology themselves are too high compared to simply using it as a service provided by someone who has invested enough resources in &lt;em&gt;commoditising&lt;/em&gt; that technology leveraging an &lt;em&gt;economy of scale&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The few companies that happen to be in that sweet spot in that precise moment during the evolution of the industry (an economist would call it the moment where the center of mass between demand and supply naturally shifts) are blessed with the privilege of monopoly or oligopoly in the industry for at least the next decade to come - which, when it comes to the IT industry, is the equivalent of a geologic eon. How long it will take until the next brief "&lt;em&gt;decentralized revolution&lt;/em&gt;" (whose only possible outcome is the rising of a new centralized ruling oligarchy) depends on how much energy the system accumulates during their reign.&lt;/p&gt;

&lt;p&gt;There is a simple reason why technology inertially moves towards centralization unless a sufficient external force is applied, and I think that this reason goes often under-mentioned:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most of the folks don't want to run and maintain their own servers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As an engineer and a hobbyist maker that has been running his own servers for almost two decades (starting with a repurposed Pentium computer in my cabinet a long time ago), as well as a long-time advocate of decentralized solutions, this is a truth that hurts. But the earlier we accept this truth, the better it is for our sanity - and the sanity of the entire industry.&lt;/p&gt;

&lt;p&gt;The average Joe doesn't want to learn how to install, run and maintain a web server and a database to manage their home automation, their communication with family and friends or their news feed unless they really have to.&lt;/p&gt;

&lt;p&gt;Just like most of the folks out there don't want to run and maintain their own power generators, their own water supply and sewage systems, their own telephone cables or their own gasoline refineries. When a product or a technology becomes so pervasive that most of the people either want it or expect it, then they'll expect somebody else to provide it to them as a service.&lt;/p&gt;

&lt;p&gt;However, people may still run their own decentralized solutions if it's required. Usually this happens when at least one of the following conditions apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;A shortage occurs&lt;/em&gt;: think of an authoritarian government cutting its population out of the most popular platforms for messaging or social network, prompting people to flock to smaller, often decentralized or federated, solutions.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;The technology for a viable centralized solution isn't out there yet&lt;/em&gt;: think of the boom of self-administered forums and online communities that preceded the advent of social networks and the Web 2.0.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Some new technology provides many more benefits compared to those provided by the centralized solution&lt;/em&gt;: think of the advantage of using a personal computer compared to using an ad-hoc, time-slotted connection to a mainframe.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;People don't trust the centralized solutions out there&lt;/em&gt;: this has definitely been the case after the Cambridge Analytica scandal brought in the spotlight the fact that free online platform aren't really free, and that giving lots of your data to a small set of companies can have huge unintended consequences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When one of these conditions apply, the industry behaves like a physical system that has collected enough energy, and it briefly jumps to its next excited state. Enough momentum may build up that leads to the next brief decentralized revolution that may or may not topple the current ruling class. However, once the initial momentum settles, new players and standards emerge from the initial excitement, and most of the people are just happy to use the new solutions as on-demand services.&lt;/p&gt;

&lt;p&gt;Unlike an electron temporarily excited by a photon, however, the system rarely goes back to its exact previous state.  The new technologies are usually there to stay, but they are either swallowed by new large centralized entities, packaged as commodified services, or they are applied in much more limited scopes than those initially envisioned.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is the Web 3.0 going to achieve decentralization?
&lt;/h3&gt;

&lt;p&gt;The different incarnations of the Web 3.0 propose different solutions to implement the vision of a more decentralized Web.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;em&gt;Semantic Web&lt;/em&gt; proposes &lt;em&gt;decentralization through presentation protocols&lt;/em&gt;. If we have defined a shared ontology that describes what an apple is, what it looks, smells and tastes like, how it can be described in words, what are its attributes, and we have also agreed to a universal identifier for the concept of "apple", then we can build a &lt;em&gt;network of content&lt;/em&gt; where we may &lt;em&gt;tag&lt;/em&gt; this concept as an ID in a shared &lt;em&gt;ontology&lt;/em&gt; in our web pages (that may reference it as an image, as a word, as a description in the form of text or audio), and any piece of code out there should be able to understand that some parts of that web page page talk about apples. In this ideal world the role of a centralized search engine becomes more marginal: the network itself is designed to be a network of servers that can understand content and context, and they can render both requests and responses from/to human/machine-friendly formats with no middlemen required. Web crawling is not only tolerated and advised: it's the way the Web itself is built.  Machines can talk to one another and exchange actual content and context, instead of HTML pages that are only rendered for human eyes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;em&gt;Web-of-Things&lt;/em&gt; proposes &lt;em&gt;decentralization through network protocols&lt;/em&gt;, as well as &lt;em&gt;scaling up the number of devices&lt;/em&gt;. If instead of having a few big computers that do a lot of things we break them up into smaller devices with lower power requirements, each doing a specific task, all part of the same shared global network, all with their own unique address, all communicating with one another through shared protocols, then the issues of fragmentation and lock-in effects of the economies of platforms can be solved by the natural benefits of the economies of scale. We wouldn't even need a traditional client/server paradigm in such a solution: each device can be both client and server depending on the context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;em&gt;Crypto-Web&lt;/em&gt; proposes &lt;em&gt;decentralization through distributed data and code storage&lt;/em&gt;. If Blockchains can store something as sensitive as an individual's financial transactions on a publicly accessible decentralized network, and in a way that is secure, transparent and anonymous at the same time, and they can even run smart contracts to manipulate these transactions, then they are surely the tool we need to build a decentralized Web.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why decentralization hasn't happened before
&lt;/h3&gt;

&lt;p&gt;We have briefly touched in the previous paragraphs why decentralization usually doesn't work on the long run. To better formalize them.&lt;/p&gt;

&lt;h4&gt;
  
  
  People don't like to run and maintain their own servers
&lt;/h4&gt;

&lt;p&gt;Let's repeat this concept, even if it hurts many of us. Decentralization often involves having many people running their own services, and people usually don't like to run and maintain their own services for too long, unless they really have to. Especially if there are centralized solutions out there that can easily take off that burden.&lt;/p&gt;

&lt;h4&gt;
  
  
  Decentralized protocols move much slower than centralized platforms
&lt;/h4&gt;

&lt;p&gt;Decentralization involves coming up with shared protocols that everybody agrees on. This is a slow process that usually requires reaching consensus between competing businesses, academia, standards organizations, domain experts and other relevant stakeholders.&lt;/p&gt;

&lt;p&gt;Once those protocols are set in stone, it's very hard to change them or extend them, because a lot of hardware and software has been built on those standards. You can come up with extension, plugins and patch versions, or introduce a breaking major version at some point, but all these solutions are usually painful and bring fragmentation issues. The greater the success of your distributed protocol, the harder it is to change, because many nodes will be required to keep up with the changes.&lt;/p&gt;

&lt;p&gt;The HTTP 1.1 protocol (a.k.a. &lt;a href="https://datatracker.ietf.org/doc/html/rfc2068"&gt;RFC 2068 standard&lt;/a&gt;) that still largely runs the Web today was released in 1997, long before many of today's TikTok celebrities were even born. On the other hand, Google developed and released gRPC within a couple of years. The IMAP and SMTP protocols that have been delivering emails for decades will probably never manage to implement official support for encryption, while WhatsApp managed to roll out support for end-to-end encryption to all of its users with a single app update. IRC is unlikely to ever find a standard way to support video chat, and its logic for sending files still relies on non-standard extensions built over DCC/XDCC, while a platform like Zoom managed to go from zero to becoming new standard de facto for the videochat within a couple of years.&lt;/p&gt;

&lt;p&gt;The same bitter truth applies to the protocols proposed both by the semantic web and the IoT web. The push towards IPv6 has moved with the pace of a snail over the past 25 years, both for political reasons (the push, at least in the past, often came from the developing world, eager to get more IP addresses for their new networks, while the US and Europe, which hoarded most of the IPv4 subnets, often preferred to kick the can down the road), but, most importantly, for practical reasons. Millions of lines of code have been written over decades assuming that an IP address has a certain format, and that it can be operated by a certain set of functions. All that code out there needs to be updated and tested, and all the network devices that run that code, from your humble smoke sensor all the way up to the BGP routers that run the backbone of the Internet, need to be updated. Even though most of the modern operating systems have been supporting IPv6 for a long time by now, there is a very long tail of user-space software that needs to be updated.&lt;/p&gt;

&lt;p&gt;While the Internet slowly embraces a new version that allows it to allocate more addresses, companies like Google and Amazon have come in to fill that gap. We still need many devices to be able to transfer messages, but instead of communicating directly to one another, they now communicate with Google's or Amazon's clouds, which basically manage them remotely, and allow communication between two different devices only if both of them support their protocols. So yes, an Internet-of-Things revolution is probably happening already, but it's unlikely to be decentralized, and it smells more like some cloud-based form of NAT.&lt;/p&gt;

&lt;p&gt;A shared standard may also be on the way (after many years) in the form of Matter, but it's a different standard than the open standards we are used to. It's a standard agreed mainly by the largest platforms on the market, with little to no involvement from the other traditional actors (academia, standards organizations, other developers).&lt;/p&gt;

&lt;h4&gt;
  
  
  Money runs the world, not protocols and technologies
&lt;/h4&gt;

&lt;p&gt;Things at these scales often move only when loads of money and human resources are poured into a field. No matter how good the new way of doing things is, or how bad the current way of doing thing is. The semantic web and its open protocols haven't really taken off because they were pursued mostly by academia, standards organizations and &lt;a href="https://www.researchgate.net/publication/307845029_Tim_Berners-Lee's_Semantic_Web"&gt;Tim Berners-Lee&lt;/a&gt;. It didn't really pick up momentum in the rest of the industry because at some point solutions like Google came up. Why would you radically change the way you build, present and distribute your web pages, when Google already provides some black magic to infer content and context and search for anything you want on the Web?  When Google already fills that gap somehow, it doesn't matter anymore whether the solution is centralized or not: most folks only care that it works well enough for their daily use.  Not only: the vision of a semantic web where content is exposed in a form that is both human-readable and machine-readable, where web crawling becomes a structural part of the new experience, would jeopardise the very reason why the world needs services like the Google search engine.  The idea of a semantic web, at least as it was proposed by the W3C, is amazing on paper, but it's hard to monetize. Why would I make my website easier for another machine to scrape and extract content from? We can arguably say that the Web has gone in the opposite direction in the meantime. Many sites have paywalls to protect their precious content from unpaid access. Many have built barriers against robots and scrapers. Many even sue companies like Google for ripping them (through Google News) of the precious profits that come from people dropping their eyeballs on the ads running on their websites. Why would a for-profit website want to invest time to rewrite its code in a way that makes its content easier to access, embed and reuse on other websites, when the whole Internet economy basically runs on ads that people can see only if they come to your website? No matter how good the technological alternatives are, they will never work if the current economic model rewards walled gardens or those that run ads that are seen by the highest number of people, especially if the system is already structurally biased towards centralization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will the crypto-web succeed where others have failed?
&lt;/h3&gt;

&lt;p&gt;The crypto-web tries to solve some of the hyper-centralization issues of the Web 2.0 by leveraging the features of Blockchain. Here are some of the proposed solutions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Monetization problem&lt;/em&gt;: the issue is addressed by simply making money a structural part of the architecture. Your digital wallet is also the holder of your identity. You can pay and get paid for content on the platform by using one of the virtual currencies it supports. The currency, the ledger of transactions and the platform are, basically, one single entity. So we have a digital currency at the core, and enough people who agree that it has some form of value, but very few ways to make use of that value - &lt;a href="https://www.bbc.com/news/world-latin-america-59368483"&gt;unless you live in El Salvador&lt;/a&gt;, you probably don't use Bitcoins to pay for your groceries. For that reason, most of the cryptocurrencies have been, over the past years, mainly instruments for investment and speculation, rather than actual ways to purchase goods and services. The new vision of the Web 3.0 tries to close this loop, by creating a whole IT ecosystem and a whole digital economy around the cryptocurrencies (the most cynical would argue that it's been designed so those who hoarded cryptocurrencies over the past years can have a way of cashing their assets, by artificially creating demand around a scarcely fungible asset). If cryptocurrencies were a solution looking for a problem, then we've now got a problem they can finally solve.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Identity&lt;/em&gt;: a traditional problem in any network of computers is how to authenticate remote users. The Web 2.0 showed the power of centralization when it comes to identity management: if a 3rd-party like Google, Facebook or Apple can guarantee (through whatever identification mechanism they have implemented) that somebody is really a certain person, then we don't have to reinvent the authentication/authorization wheel from scratch on every different system. We delegate these functionalities to a centralized 3rd-party that &lt;em&gt;owns&lt;/em&gt; your digital identity through mechanisms like OAuth2. By now, we also all know the obvious downsides of this approach - loss of privacy, single-point of failure (if your Google/Facebook account gets blocked/deleted/compromised then you won't be able to access most of your online services), your digital identity is at the mercy of a commercial business that doesn't really care a lot about you, Big Button Brother effect (no matter what you visit on the Internet, you'll always have some Google/Facebook button/tracker/hidden pixel somewhere that sees what you're doing). The crypto-web proposes a solution that maintains the nice features of a federated authentication, while being as anonymous as you can get. Your digital wallet becomes a proxy to your identity. If you can prove that you are the owner of your wallet (usually by proving that you can sign data with its private key), then that counts as authentication. However, since your wallet is just a pair of keys registered on the Blockchain with no metadata linked to you as a physical person, this can be a way to authenticate you without identifying you as a physical person. An interesting solution is also provided for the problem of "&lt;em&gt;what if a user loses their password/private key/the account gets compromised?&lt;/em&gt;" in the form of &lt;a href="https://vitalik.ca/general/2021/01/11/recovery.html"&gt;social recovery&lt;/a&gt;. In the world of Web 2.0, identity recovery isn't usually a big deal. Your digital identity is owned by a private company, which also happens to know your email address or phone number, or it also happens to run the OS on your phone, and they have other ways to reach out to you and confirm your identity. In the world of crypto, where an account is just a pair of cryptographic keys with no connection to a person, this gets tricky. However, you can select a small circle of family members or friends that have the power to confirm your identity and migrate your data to a new keypair.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Protocols and standards&lt;/em&gt;: they are replaced by the Blockchains. Blockchains are ledgers of transactions, and the largest ones (such as Bitcoin and Ethereum) are unlikely to be going anywhere because a lot of people have invested a lot of money into them. Since Blockchains can be used a general-purpose storage for data and code, then we've got the foundations for a new technology that doesn't require us to enforce new protocols that may or may not be adopted.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Scalability&lt;/em&gt;: decentralized systems are notoriously difficult to scale, but that's simpler for Blockchains. Everybody can run a node, so you just add more nodes to the network. If people don't want to run their nodes, then they can use a paid service (like &lt;a href="https://infura.io/"&gt;Infura&lt;/a&gt;, &lt;a href="https://www.alchemy.com/"&gt;Alchemy&lt;/a&gt;) to interact with the Blockchain.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far so good, right? Well, let's explore why these promises may be based on a quite naive interpretation of how things work in reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blockchains are scalability nightmares
&lt;/h2&gt;

&lt;p&gt;Let's first get the elephant out of the room. From an engineering perspective, a Blockchain is a scalability nightmare.  And, if implemented in its most decentralized way (with blocks added by nodes through &lt;a href="https://ethereum.org/en/developers/docs/consensus-mechanisms/pow/"&gt;&lt;em&gt;proof-of-work&lt;/em&gt;&lt;/a&gt;), they are environmental and geopolitical disasters waiting to happen. This is because the whole assumption that anybody can run their own node on a large Blockchain is nowadays false.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running a Blockchain is a dirty business
&lt;/h3&gt;

&lt;p&gt;In the early days of the crypto-mania, anybody could indeed run a &lt;a href="https://www.tomshardware.com/how-to/mine-cryptocurrency-raspberry-pi"&gt;small clusters of spare Raspberry Pis&lt;/a&gt;, mine Bitcoins and make some money on the side. Nowadays, big mining companies have installed server farms in the places where energy is the cheapest, such as &lt;a href="https://fortune.com/2021/05/02/bitcoin-mining-hashrate-china-inner-mongolia-ban/"&gt;near Inner Mongolia's hydroelectric dams&lt;/a&gt;, &lt;a href="https://fortune.com/2021/04/20/bitcoin-mining-coal-china-environment-pollution/"&gt;near Xinjiang's coal mines&lt;/a&gt; or &lt;a href="https://edition.cnn.com/2022/01/07/investing/bitcoin-mining-kazakhstan-protests-impact-intl-hnk/index.html"&gt;leveraging Kazakhstan's cheap natural gas&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The author of the post on Tom's Hardware mentions that in February 2021 he made $0.00015569 after running mining software on his Raspberry Pi 4 for 8 hours. His hash rate (the number of block hashes added to the Blockchain per unit of time) varied from 1.6 H/s to 33.3 H/s. For comparison, the average hash rate of the pool was 10.27 MH/s. That's 3-4 million times more than the average hash rate of a Raspberry Pi.&lt;/p&gt;

&lt;p&gt;The examples I mentioned (Inner Mongolia, Xinjiang and Kazakhstan) are not casual.  At its peak (around February 2020) &lt;a href="https://ccaf.io/cbeci/mining_map"&gt;Chinese miners contributed more than 72% of the Bitcoin hashes&lt;/a&gt;. The share hovered between 55% and 67% until July 2021, when the government imposed a nationwide crackdown on private crypto-mining. The motivations, as usual, are complex, and they obviously include the government's efforts of &lt;a href="https://www.wsj.com/articles/china-creates-its-own-digital-currency-a-first-for-major-economy-11617634118"&gt;creating a digital yuan&lt;/a&gt;.  However, the fact that private miners scoop up a lot of cheap energy before it can get downstream to the rest of the grid, especially during times of energy supply shortages and inflationary pushes on the economy, has surely played a major role in Beijing's decisions.&lt;/p&gt;

&lt;p&gt;The fall of Beijing in the crypto market left a void that other players have rushed to fill in. The share of Bitcoin hashes contributed by nodes in Kazakhstan has gone from 1.42% in September 2019 up to 8.8% in June 2021, when China announced its crackdown on crypto. Since then, it climbed all the way up to 18% in August 2021 - and that's the latest available data point on the Cambridge Bitcoin Electricity Consumption Index. From more recent news, higher demand for Kazakh gas, mixed with the high inflation, pushed energy prices up, triggering mass protests and the resignation of the government. &lt;/p&gt;

&lt;h3&gt;
  
  
  The curse of the proof-of-work
&lt;/h3&gt;

&lt;p&gt;Why do decentralized Blockchains consume so much energy that they can impact the energy demand and production of whole countries, as well as destabilise whole geopolitical landscapes?&lt;/p&gt;

&lt;p&gt;The Bitcoin Blockchain processes &lt;a href="https://ycharts.com/indicators/bitcoin_transactions_per_day"&gt;less than 300 thousands transactions per day&lt;/a&gt;. For a comparison, as of 2019 the credit card circuits processed &lt;a href="https://www.cardrates.com/advice/number-of-credit-card-transactions-per-day-year/"&gt;about 108.6 millions transactions per day&lt;/a&gt; in the US alone.&lt;/p&gt;

&lt;p&gt;We therefore have a proposed new approach whose underlying infrastructure currently processes less than 0.28% of the transactions processed in the US alone. Interestingly, the volumes of transactions processed by Visa and Mastercard have never caused geopolitical instabilities nor drained the energy supply of whole countries. How come?&lt;/p&gt;

&lt;p&gt;To answer this question, we have to understand how Blockchains (or, at least, the most popular decentralized flavour initially proposed by &lt;a href="https://bitcoin.org/bitcoin.pdf"&gt;Natoshi Sakamoto's paper&lt;/a&gt;) work under the hood.&lt;/p&gt;

&lt;p&gt;We have often heard that a Blockchain is &lt;em&gt;secure by design&lt;/em&gt;, and it allows the distributed creation of a ledger of transactions among peers that don't necessarily trust one another. This is possible through what is commonly known as &lt;a href="https://ethereum.org/en/developers/docs/consensus-mechanisms/pow/"&gt;&lt;em&gt;proof-of-work&lt;/em&gt;&lt;/a&gt;. Basically, any node can add new blocks to the distributed chain, provided that the operation satisfies two requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The new blocks are &lt;em&gt;cryptographically linked&lt;/em&gt; to one another and consistent with the hashes of the previous latest block. A Blockchain is, as the name suggest, a &lt;em&gt;chain of blocks&lt;/em&gt;. Each of these blocks contains a transaction (e.g. &lt;em&gt;the user associated to the wallet ABC has paid 0.0001 Bitcoins to the user associated to the user associated to the wallet XYZ on time t&lt;/em&gt;), and a pointer with the hash of the latest block in the chain at the time &lt;em&gt;t&lt;/em&gt;. Any new added blocks must satisfy this contraint: basically, blocks are always added in consecutive order to the chain, they cannot be removed (that would require all the hashes of the following blocks to be updated), and this is usually a good way of ensuring &lt;em&gt;temporal consistency&lt;/em&gt; of events on a distributed network.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In order to add new blocks, the &lt;em&gt;miner&lt;/em&gt; is supposed to solve a numeric puzzle to prove that they have spent a certain amount of computational resources before requesting to add new blocks. This puzzle is defined in such a way that:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;2.1. It's hard to find a solution to it (or, to be more precise, a solution can only be found through CPU-intensive brute force);&lt;/p&gt;

&lt;p&gt;2.2. It's easy to verify if a solution is valid (it usually involves calculating a simple hash of a string through an algorithm like SHA-1).&lt;/p&gt;

&lt;p&gt;The most popular approach (initially proposed &lt;a href="https://en.wikipedia.org/wiki/Hashcash"&gt;in 1997 by Hashcash&lt;/a&gt; to prevent spam/denial-of-service, and later adopted by Bitcoin) uses &lt;em&gt;partial hash inversions&lt;/em&gt; to verify that enough work has been done by a node before accepting new data from it. The network periodically calibrates the &lt;em&gt;difficulty&lt;/em&gt; of the task by establishing the number of hash combinations in the cryptographic proofs provided by the miners (on Bitcoin, this usually means that the target hash should start with a certain number of zeros).  This is tuned in such a way that the &lt;em&gt;block time&lt;/em&gt; (i.e.  the average time required to add a new block to the network) remains constant. If the only way to find a solution to the riddle is by brute-forcing all the possible combinations, then you can tune the time required to solve the riddle by changing the total number of combinations that a node should explore.  If a lot more powerful mining machines are added to the pool, then it'll likely push down the block hashing time, and therefore at some point the network may increase the difficulty of the task by increasing the number of combinations a CPU is supposed to go through.&lt;/p&gt;

&lt;p&gt;For example, let's suppose that at some point the network requires the target hashes to start with 52 binary zeros (i.e. 13 hexadecimal zeros). On the original Hashcash network, an email could be sent on January 19, 2038 to &lt;code&gt;calvin@comics.net&lt;/code&gt; if it had a header structured 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;X-Hashcash: 1:52:380119:calvin@comics.net:::9B760005E92F0DAE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;X-Hashcash&lt;/code&gt; is the header of the string to be used as a cryptographic challenge;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;52&lt;/code&gt; is the number of leading binary zeros that the target hash is supposed to have;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;380119&lt;/code&gt; represents the timestamp of the transaction;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;9B760005E92F0DAE&lt;/code&gt; is the hexadecimal digest calculated by the miner that, appended to the metadata of the block, satisfies the hashing requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we take the string above (except for the &lt;code&gt;X-Hashcash:&lt;/code&gt; header title and any leading whitespaces) and we calculate its SHA-1 hash we get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000000000000756af69e2ffbdb930261873cd71
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hash has 13 leading hexadecimal zeros (i.e. 52 binary zeroes), and it therefore satisfies the network's hashing requirements: the node is allowed to process the requested information.&lt;/p&gt;

&lt;p&gt;Here you go: you have an algorithm that makes it computationally expensive to add new data (a miner has to iterate through zillions of combinations to find a string whose hash satisfies a certain condition), but computationally easy to verify that the hash of a block is correct (you just have to calculate a SHA hash and verify that it meets the condition).&lt;/p&gt;

&lt;p&gt;Miners are usually rewarded for their work by being paid a certain fee of the amount of the processed transaction. It's easy to see how early miners who invested the unused power of a laptop or a Raspberry Pi to mine Bitcoins managed to make quite some money on the side, while making a profit out of mining nowadays, given how high the bar has moved over the past years, is nearly impossible unless you invest on some specialised boards that suck up a lot of power.&lt;/p&gt;

&lt;p&gt;The proof-of-work provides a secure mechanism to process information among untrusted parties. A malicious actor who wants to add invalid data to the chain (let's say, move one Bitcoin to their wallet from another wallet that hasn't really authorised the transfer) will first have to crack the private key of the other wallet in order to sign a valid transaction, then invest enough computational power to add the block - and this usually means being faster in average than all the nodes on the Blockchain, which already sets a quite high bar on large Blockchains. The new block needs to be accepted through shared consensus mechanisms from a qualified majority of the nodes on network, and usually long chains are more likely to be added than short ones - meaning that you have to either control a large share of the nodes on the network (which is usually unlikely unless you have invested the whole GDP of a country on it), or build a very long chain, therefore spending more power in average than all the other nodes on the network, to increase the chances that your rogue block gets included.  Not only, but the new rogue block needs to be cryptographically linked to &lt;em&gt;all the previous and next transactions in the ledger&lt;/em&gt;. So the rogue actor will basically have to forever run a system that is faster than all the other nodes in order to "keep up" with the lie. This is, in practice, impossible. In some very rare cases, rogue blocks may eventually be added to the Blockchain. But, when that happens, the network can usually spot the inconsistency within a short time and fork the chain (i.e. cut a new chain that excludes the rogue block and any successive blocks).&lt;/p&gt;

&lt;p&gt;This also means that rogue nodes are implicitly punished by the network itself.  An actor who tries to insert invalid nodes will eventually be unable to keep up their lie, but they will have wasted the energy required to calculate the hashes necessary to add those blocks. This means that they'll have wasted  &lt;em&gt;a lot&lt;/em&gt; of energy without being rewarded in any way.&lt;/p&gt;

&lt;p&gt;A problem that wasn't originally addressed by the Hashcash proposal (and wasn't addressed by earlier cryptocurrencies either) was that of &lt;em&gt;double-spending&lt;/em&gt;.  This &lt;a href="https://www.investopedia.com/ask/answers/061915/how-does-block-chain-prevent-doublespending-bitcoins.asp"&gt;has been addressed by Bitcoin&lt;/a&gt; by basically implementing a P2P validation where a distributed set of nodes needs to reach consensus before a new block is added to the chain, and temporal consistency of transactions is enforced by design with a double-linked chain of nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The proof-of-work is a proof-of-concept that was never supposed to scale
&lt;/h3&gt;

&lt;p&gt;Now that we know how a Blockchain manages to store a distributed ledger of transactions among untrusted parties, it's easy to see why it's such a scalability nightmare. Basically, the whole system is designed around the idea that its energy consumption increases with the number of transactions processed by the network. Not only: since the whole algorithm for adding new blocks relies on a massively CPU-intensive brute-force logic to calculate a hash that start with a certain number of leading zeros, the amount of CPU work (and, therefore, electricity usage) &lt;strong&gt;is&lt;/strong&gt; basically the underlying currency of the network.  If the average block hashing time decreases too much, then the likelihook of success for rogue actors who want to add invalid transactions and invest enough in hardware resources increases. We therefore need to keep the cost of mining high so that it's technically unfeasible for malicious actors to spoof transactions.&lt;/p&gt;

&lt;p&gt;As engineers, we should be horrified by a technical solution whose energy requirements increase linearly (or on even steeper curves) with the amount of data it processes per unit of time. Managing the scale of a system so that the requirements to run it don't increase at the same pace as the volumes of data that it processes is corner-stone of engineering, and Blockchains basically throws this principle out of window while trying to convince us that their solution is better. Had the "Blockchain revolution" happened under other circumstances, we'd be quick to dismiss its proposals as hype in the best case, and outright scam in the worst case. In an economy that has to increasingly deal with limited supply of resources and increasingly tighter energy constraints, a solution that basically requires wasting CPU power just to add a new block (not only: a system where the amount of electric energy spent is the base currency used to reward nodes) is not only unscalable: it's immoral.&lt;/p&gt;

&lt;p&gt;Not only: the requirement to keep a block mining time constant means that, &lt;em&gt;by design&lt;/em&gt;, the network can only handle a limited amount of transactions. In other words, we are capping the throughput of the system by design in order to maintain the system secure and consistent. I'm not sure how anybody with any foundation of logical reasoning can believe that such a system can scale to the point that it replaces the way we do things today.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proof-of-stake to the rescue?
&lt;/h3&gt;

&lt;p&gt;The scalability issues of the proof-of-work mechanism are well-known to anybody who has any level of technical clue. Those who deny it are simply lying.&lt;/p&gt;

&lt;p&gt;In fact, Ethereum is supposed to migrate during 2022 &lt;a href="https://www.msn.com/en-us/money/markets/ethereum-may-linger-if-the-proof-of-stake-transition-takes-too-long/ar-AASxo0Z"&gt;away from proof-of-work and towards a &lt;em&gt;proof-or-stake&lt;/em&gt; mechanism&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Under &lt;a href="https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/"&gt;such a mechanism for distributed consensus&lt;/a&gt;, miners don't compete with one another to add new blocks, and they aren't rewarded on the basis of the electric energy consumed. Instead, they have a certain amount of &lt;em&gt;tokens&lt;/em&gt; that represent &lt;em&gt;votes&lt;/em&gt; that they can use to &lt;em&gt;validate&lt;/em&gt; transactions added to the network. Under the newly proposed approach, for example, a node would have to spend 32 ETH in order to become a &lt;em&gt;validator&lt;/em&gt; on the network (&lt;em&gt;invested stake&lt;/em&gt;). Whenever a node tries to add a new transaction to the network, a pool of validating nodes is chosen a random. The behaviour of a validator determines the value of its stake - in other words, rogue nodes would be &lt;em&gt;punished by karma&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Nodes are rewarded (their stake is increased) when they add/validate new valid transactions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nodes can be punished (their stake is decreased), for example, when they are offline (as it is seen as a failure to validate blocks), or when they add/validate invalid transactions. Under the worst scenario (collusion), a node could lose its entire stake.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On paper, this approach should guarantee the same goal as proof-of-work (reaching a shared consensus among a distributed network of untrusted parties), without burning up the whole energy budget of the planet in the process. Why don't we just go all-in on the proof-of-stake then?&lt;/p&gt;

&lt;p&gt;Well, one reason is that it's a system that is definitely less tested on wide networks compared to proof-of-work. PoW works because it leverage physical constraints (like the amount of consumed energy) to add new blocks. PoS removes those constraints, it proposes instead a stake-based system that leverages some well-documented mechanisms of reward and punishment from game theory, but once the physical constraint is removed there are simply too many things to take into account, and different ways a malicious actor may try and trick the system.&lt;/p&gt;

&lt;p&gt;A common problem of PoS systems is that of a &lt;a href="https://www.investopedia.com/terms/1/51-attack.asp"&gt;&lt;em&gt;51% attack&lt;/em&gt;&lt;/a&gt;. An attacker who owns 50%+1 of the share of stakes of a currency can easily push all the transactions that they want - consensus through qualified majority is basically guaranteed. The risk for this kind of attack is indeed very low in the case of Ethereum - one Ethereum coin is expensive, buying such a large share of them is even more expensive, and anyone who invests such a large amount has no interest in fooling the Blockchain, cause a loss of trust and therefore erode the value of their own shares. However, this risk is still very present in smaller Blockchains. PoS is a mechanism that probably only works in the case of large Blockchains where it's expensive to buy such a large share of tokens. On smaller Blockchains with a lower barrier to buy a majority stake it can still be a real issue.&lt;/p&gt;

&lt;p&gt;There is another issue, commonly known as &lt;a href="https://golden.com/wiki/Nothing-at-stake_problem"&gt;&lt;em&gt;nothing-at-stake&lt;/em&gt;&lt;/a&gt;. This kind of issues can occur when a Blockchain &lt;em&gt;forks&lt;/em&gt; - either because of malicious action or because of two nodes actually propose two blocks at the exact same time, therefore making temporal causality tricky.&lt;/p&gt;

&lt;p&gt;When this happens, it's in the interest of the miners to keep mining both the chains. Suppose that they keep mining only one chain, and the other one is eventually the one that picks up steam: the miner's revenue will sink down. In other words, mining all the forks of a chain maximises the revenue of a miner.&lt;/p&gt;

&lt;p&gt;Suppose now that an attacker wants to attempt a double-spending attack. They may do so by attempting to create a fork in the Blockchain (for example, by submitting two blocks simultaneously from two nodes that they own) just before spending some coins. If the attacker only keeps mining their fork, while all the other miners act in their self-interest and keep mining both the forks, then the attacker's fork eventually becomes the longest chain, even if the attacker only had a small initial stake.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://yanmaani.github.io/proof-of-stake-is-a-scam-and-the-people-promoting-it-are-scammers/"&gt;simpler version of this attack&lt;/a&gt; is the one where a validator cashes out their whole stake but the network does not revoke their keys. In this case, they may still be able to sign whatever transactions that they wish, but the system may not be able to punish them - the node has &lt;em&gt;nothing at stake&lt;/em&gt;, therefore it can't be punished by slashing their stake.&lt;/p&gt;

&lt;p&gt;Ethereum proposes to mitigate these issues by linking the stake onto the network to an actual financial deposit. If an attacker tries to trick the system, they may lose real money that they have invested.&lt;/p&gt;

&lt;p&gt;There is, however, a problem with both the mitigation policies proposed by Ethereum.  Their idea is basically to disincentivise malicious actors by increasing the initial stake required to become a validator. The idea is that, if the stake you lose is higher than the money you can make by tricking the system, you'll think twice before doing it. What this means is that, in order for the system to work, you need to increase the initial barriers as the value of your currency (and the number of transaction it processes) increases. So, if it works, it'll basically turn the Blockchain into a more centralized system where a limited amount of nodes can afford to pay the high cost required to validate new blocks (so much for the decentralization dream). As of now, the entry fee to become a validating node is already as high as 32 ETH - more than $100k. As the system needs to ensure that the stake a node loses for rogue behaviour is higher than the average gain they can make from tricking the system, this cost is only likely to go up, making the business of becoming a validating node on the Blockchain very expensive. If it doesn't, if the value of the cryptocurrency plunges and the perceived cost of tricking the system gets lower, then there's nothing protecting the system from malicious actors.&lt;/p&gt;

&lt;p&gt;In other words, proof-of-stake is a system that works well under some very well defined assumptions - and, when it works, it's probably doomed to swing towards a centralized oligarchy of people loaded with money anyways.&lt;/p&gt;

&lt;h3&gt;
  
  
  How about data storage scalability?
&lt;/h3&gt;

&lt;p&gt;Another scalability pain point of Blockchains is on the data storage side.  Remember that any mining/validating node on the network needs to be able to validate any transaction submitted to the network. In order to perform this task, they need a complete synchronized snapshot of the whole Blockchain, starting with its first block all the way up to the latest one.&lt;/p&gt;

&lt;p&gt;If 300k blocks per day are submitted, and supposing that the average size of a block is 1 KB, this translates into a data storage requirement of about 100 GB per year just to store the ledger of transactions.&lt;/p&gt;

&lt;p&gt;As an engineer, suppose that you have a technological system that requires thousands of nodes on the network to process about 0.2% of the volumes of transactions currently processed by Visa/Mastercard/Amex in the US alone. The amount of nodes required on the network increases proportionally with the amount of transactions that the network is able to process, because we want the process of adding new blocks to be artificially CPU-bound. Moreover, each of these nodes has to keep an up-to-date snapshot of the whole history of the transactions, and it requires about 100 GB for each node to store a year of transactions. Since every node has to keep an up-to-date snapshot of the whole ledger, you can't rely on popular scalability solutions such as data sharding or streaming pipelines. Not only, but adding a new block to this "decentralized filesystem" requires that you run some code that wastes CPU cycles in order to solve a stupid numeric puzzle with SHA hashes. How would you scale up such a system to manage not only the amount of financial transactions currently handled by the existing solutions, but run a whole web of decentralized apps on it? I think that most of the experienced engineers out there would feel either uncomfortable or powerless if faced with such a scalability challenge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Castles built on sand
&lt;/h3&gt;

&lt;p&gt;The Blockchain has promised us a system where any nodes could join the network, add and validate transactions (&lt;em&gt;distributed promise&lt;/em&gt;) in a scalable and secure way, and that system could soon disrupt not only the way we manage financial transactions, but the way we run the entire Web. This whole promise was based around mechanisms to reach &lt;em&gt;shared consensus&lt;/em&gt; on distributed networks.&lt;/p&gt;

&lt;p&gt;It turns out that both those mechanism (proof-of-work and proof-of-stake) aren't ready to fullfil those promises.&lt;/p&gt;

&lt;p&gt;Proof-of-work is an environmental and scalability disaster by design, with capped throughput, ability to process only a tiny fraction of the transactions we process nowadays on traditional circuits, and the amount of energy wasted to add new blocks being the fundamental currency of the network.&lt;/p&gt;

&lt;p&gt;Proof-of-stake is more promising, but it hasn't yet fully addressed its underlying security issues - it has proposed mitigation measures, not real structural solutions - and it's not been tested on large scales yet.&lt;/p&gt;

&lt;p&gt;Moreover, both the solutions are doomed, by design, to break their promises about decentralizations.&lt;/p&gt;

&lt;p&gt;As the example of Bitcoin clearly shows, when the amount of resources invested by miners increases (because the value of the currency itself increases, therefore mining blocks becomes more profitable), the entry barriers for mining also increase. Unless you have a dedicated server farm located in some of the places on the planet where energy is cheapest, you probably won't make much money out of it. So mining basically becomes a game played by a limited amount of players, with a long tail left to eat breadcrumbs.&lt;/p&gt;

&lt;p&gt;When it comes to proof-of-stake, instead, the security of the network, as the number of nodes or the value of the currency increases, can only be guaranteed by increasing the initial deposit required by nodes who want to join as validators. As the margin of revenue increases, you can bet that more affluent players will join the club, eventually pushing up the initial stake and therefore increasing the entry barriers - it's basically a network where the actors with more power are the ones who can afford to pay more money. So much for the idea of a decentralized and more equitable network.&lt;/p&gt;

&lt;p&gt;In other words, it can be proven that a Blockchain can't scale to the demands of throughput required by modern networks without either giving up on one of its promises (either of security of true decentralization), or causing unacceptable side effects. So why are we even talking about it as if it actually was a serious option to consider?&lt;/p&gt;

&lt;p&gt;We are not in the initial days of the technology either - some rough corners would be a bit more tolerable if that was the case. Sakamoto's paper was published 12 years ago - an eternity when it comes to technology. In the meantime, Bitcoin's unsustainability has been proved again and again, to the point where it currently takes the whole yearly energy output of a country like Argentina to mine only 0.2% of the credit card transactions currently processed in the US alone. Its high energy cost has already had consequences on countries like China and Kazakhstan. 12 years down the line, we don't have yet a proof-of-work 2.0 that could address or at least mitigate its own unsustainability issues. And we have another proposed solution (proof-of-stake) that is either doomed to make Blockchains more centralized than the traditional circuits, or it is doomed to dilute its security constraints with "mitigation actions" for commonly known issues. Would we accept Visa or Mastercard to build their own security around "structural assumptions" and "mitigation policies"?  Would we feel confident such an infrastructure to be in charge of our money or personal data? Then, again, we should stop talking of Blockchains as if they were a serious solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Web 3.0 is already much more centralized than we think
&lt;/h2&gt;

&lt;p&gt;This is an obvious corollary of the conclusions we have reached in the previous paragraph.&lt;/p&gt;

&lt;p&gt;In order to build any kind of web, you need to be able to build software on it.  In the crypto world, software takes the shape of &lt;a href="https://ethereum.org/en/dapps/"&gt;&lt;em&gt;dApps&lt;/em&gt;&lt;/a&gt; (distributed apps).&lt;/p&gt;

&lt;p&gt;dApps basically leverage one of the core features of Ethereum (&lt;a href="https://www.ibm.com/topics/smart-contracts"&gt;&lt;em&gt;smart contracts&lt;/em&gt;&lt;/a&gt;) to run software that can read or manipulate the Blockchain.&lt;/p&gt;

&lt;p&gt;Some example standard use-cases for smart contracts are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You want to create a logic that moves 10% of a certain sum from your wallet to another wallet every month until the original sum is exhausted (payments in &lt;em&gt;installments&lt;/em&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You want to create a logic that moves a certain sum to another wallet if the value of an underlying asset (e.g. a stock, a bond or another financial instrument) goes below/above a certain value (a &lt;em&gt;derivative&lt;/em&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The standard use-case for a smart contract, in other words, is to model a financial instrument that manipulates the ledger and moves money across wallets if/when a certain set of conditions is met.&lt;/p&gt;

&lt;p&gt;The whole idea behind the Web 3.0 is to leverage these technological instruments to build all kind of software. In theory, this solution solves the problem of authentication by design (the identity of users is mapped to their digital wallets, which is something recognized by everyone on the Blockchain, yet they mask the user's real identity unless they want to reveal it), while providing a stable secure and immutable storage by design under the form of a Blockchain.  Not only, but financial transactions between users can happen on the fly, without having to manage complex integrations for payments, without having to give up the control of the money flow to a handful of major players. This would ideally open a whole world of decentralized micro-payments that don't rely on intermediary banks with their fees.&lt;/p&gt;

&lt;p&gt;Not only: the Blockchain allows you to mint &lt;em&gt;Non-Fungible Tokens&lt;/em&gt; (&lt;em&gt;NFT_s).  Unlike any other tokens, which are equally _fungible&lt;/em&gt; (i.e. you can swap any token A for a token B, and everybody agrees that they have the same value, just like a $1 bill has the same value of another $1 bill), NFTs are minted only once, and they can be univocally linked to a digital asset (such as a media file or game mod). A user who &lt;em&gt;buys&lt;/em&gt; that token basically buys the &lt;em&gt;underlying asset&lt;/em&gt;, and since the transaction can be easily verified by anyone on the Blockchain, everybody can agree that the user owns that asset. This would open a whole new set of revenue streams for artists and digital creators.&lt;/p&gt;

&lt;p&gt;So far so good, right? Well, as you may have guessed, there are a few problems with this vision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real dApps come with real gatekeepers
&lt;/h3&gt;

&lt;p&gt;We have extensively analysed why Blockchains are no longer that small niche world where everyone can run their server on a Raspberry Pi and seriously be a part of a distributed network.  The entry barriers to run a node that is seriously able to push blocks to the network are very high - either because of high costs in terms of energy/hardware requirements (PoW), or high financial costs (PoS, realistic forecast).&lt;/p&gt;

&lt;p&gt;So, if the cost of running a node on a Blockchain is high, then how do we interact with it in our software? How do we actually run these marvelous smart contracts? Well, by delegating access to the Blockchain to a few major players that provide API access to interact with it. Right now, the market is rather consolidating around &lt;a href="https://infura.io/"&gt;Infura&lt;/a&gt; and &lt;a href="https://www.alchemy.com/"&gt;Alchemy&lt;/a&gt;. Even when it comes to authentication-over-wallet, most of the distributed applications nowadays delegate the task to &lt;a href="https://metamask.io/"&gt;MetaMask&lt;/a&gt;, which already provides integrations with most of the popular wallets out there. MetaMask itself, under the hood, simply routes API calls to Infura.&lt;/p&gt;

&lt;p&gt;But wait a minute - the whole premise of this crypto-web was that we wanted more decentralization. We were tired of delegating tasks such as user authentication, software execution and financial transactions to a handful of major Web 2.0 technological providers. So we decided to invest tons of resources and efforts to design a decentralized system with shared trust management. How come then have we decided to replace the Google/Facebook login button with a MetaMask login button, and running software on Google/Amazon/Microsoft's clouds with running it over the APIs provided by Infura/Alchemy?&lt;/p&gt;

&lt;p&gt;If you have come so far in the article, you probably already know the answer to this question: it was inevitable. Technological systems already have a built-in bias towards centralization. If you model a system in such a way that its entry barriers also scale up with its size, then such a bias becomes inevitability.  The reasons, again, are a corollary of the previous paragraphs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;On average, people are lazy. They don't want to run servers unless they really have to. So, if somebody provides them with easy access to some services, and the subscription/on-demand cost of these services is lower than the cost of running their own hardware and software, people will inevitably tend to favour that option. Especially when the system is designed in such a way that being an element of the distributed network requires you to run a small data center and add at least 100 GB of physical storage every year.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On average, developers are lazy (corollary of the previous point). Sure, they may initially want to experiment with new technologies and frameworks. But once the cost to competitively run production-ready hardware and software increases because the complexity of the system itself increases (because there are many cryptocurrencies and types of wallets to support, or because there are many protocols and APIs to support, or because the cost of running a node or investing an initial stake is too high), then they may just opt for someone who offers themselves as an intermediary. If somebody can solve the problems of user authentication, data validation and data integration for us, then we're likely to use their services, APIs or SDKs, instead of reinventing the wheel.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not only this flavour of Web 3.0 was doomed to swing back towards centralization, but its proposed approached is even much more centralized than today's Web.&lt;/p&gt;

&lt;p&gt;Nowadays, nobody prevents you from buying your own domain, running your own server, installing Apache/nginx (or just use an off-the-shelft solution like Wordpress), maybe an integration with PayPal/Stripe/Adyen, and having your own website ready to accept users and payments. You can even store your own database of users, so you don't need to rely on Google/Facebook login buttons and trackers. No fancy clouds involved, no centralization. Sure, the average Joe that isn't that fluent with HTML, JavaScript or DNS management may still prefer an off-the-shelf centralized solution, but nothing prevents anybody with the right tools and skills from running a website without relying on the Web 2.0 mafia.&lt;/p&gt;

&lt;p&gt;In the case of the Web 3.0, instead, dApps can run their logic only if they run on a node that is part of the network. Running a node on the network while being profitable, as we have extensively explored, is practically an out-of-reach task even for the most experienced and affluent engineer. So basically, &lt;em&gt;by design&lt;/em&gt;, you have no way of running software on the Web 3.0 other than delegating the task to a handful of &lt;em&gt;gatekeepers&lt;/em&gt;. Not only: since these gatekeepers manage &lt;em&gt;all&lt;/em&gt; the transactions between you and the Blockchain, you have no choice but trusting what they say - their API responses are basically the only source of truth. And you have no options to test your software locally either - either you run your own local Blockchain (which is highly unpractical), or you run your software in their sandboxes. Imagine a world where all the interactions with any website out there require all the HTTP requests and responses to be proxied through Google's servers, and you have no options outside of Google's cloud even to run/test your software. Those who dream of the Web 3.0 as a place to escape the current technological oligopoly should probably ponder the fact that they are just shifting power to another oligopoly with even higher entry barriers and concentration of power.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transaction fees are actually higher than most of the credit card circuits
&lt;/h3&gt;

&lt;p&gt;How about micro-payments and escaping the jail of credit card circuits with their transaction fees?&lt;/p&gt;

&lt;p&gt;Well, one should keep in mind that Blockchains are systems made of &lt;em&gt;miners&lt;/em&gt; that consume energy in order to add transactions. In order to incentivise miners to run nodes on the Blockchain, we need to provide them with a reward. This rewards takes the shape of a &lt;em&gt;fee&lt;/em&gt;, which can be either fixed or proportional either to the transacted amount or the amount of consecutive blocks that are mined.&lt;/p&gt;

&lt;p&gt;Not only: remember that the network has to ensure that it's always profitable enough for miners to run their nodes. If that's no longer the case, the Blockchain may die off - nobody likes to waste electric power with no prospect of making a profit. This means that the fees on a Blockchain are constrained to the demand/supply mechanisms that rule any other markets. A shortage of miners compared to the demand for transactions (either because mining a block isn't profitable when weighed against its cost, or because the network has to handle a spike in the number of transactions, or because a whole country decides to crack down on cryptocurrencies and take supply out of the pool) means that the average fee needs to increase as well - you need to pay miners better if you want to attract more of them. This cost is simply offloaded to the party that initiated the transaction.&lt;/p&gt;

&lt;p&gt;Nowadays, &lt;a href="https://ycharts.com/indicators/bitcoin_average_transaction_fee"&gt;the average transaction fee&lt;/a&gt; for Bitcoin is around $1.7 (remember, to process only a tiny fraction of the transactions processed by today's circuits). But this can vary widely depending on the supply of miners on the network. Shortage of miners equals higher fees: the few miners on the network will process the transactions that offer higher gains. In April 2021, for example, the average transaction fee to process Bitcoins spiked at nearly $63. How can you build any solution to handle micro-payments around a system whose transaction fees can go from a few cents to $60 depending on the supply of miners on the network? How is it even financially feasible to move $0.05 from my wallet to a band's digital wallet after streaming an mp3 on their website, if the fee to process such a transaction can go all the way from 20 to 1000 times the value of the transacted amount?&lt;/p&gt;

&lt;p&gt;Yes, micro-payments are a real technological problem that deserves to be solved.  Yes, today's solutions to handle online payments aren't perfect. But Blockchains only seem to make the problem worse. What we may need a shared protocol to handle payments - some parts of it have already been envisioned, like the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402"&gt;HTTP 402 code&lt;/a&gt;, or the proposed &lt;a href="https://www.w3.org/TR/payment-request/"&gt;W3C standard for handling payments&lt;/a&gt;. Yes, the Web 3.0 proposes similar solutions through shared protocols, but I'd rather extend that we already have (HTTP and HTML) instead of relying on something completely new, especially if that new solution comes with so many drawbacks and shortcomings.&lt;/p&gt;

&lt;p&gt;Let's come to another promise that the Web 3.0 can't fullfil: data storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Blockchain does not really store data
&lt;/h2&gt;

&lt;p&gt;Anybody who tells you that your data will be safely stored on a Blockchain is a scammer.&lt;/p&gt;

&lt;p&gt;A Blockchain was never supposed to store general-purpose data. Hell, with a throughput of 300k blocks per day, a cost per block that equals the daily energy demand of a small apartment, the requirement for all the nodes to hold an up-to-date snapshot of the whole database and no margins for data sharding, it'd be the worst and most unscalable data storage that I can think of. The only thing that could outcompete it would be &lt;a href="https://xkcd.com/378/"&gt;storing data through butterflies&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Blockchains were designed to store financial &lt;em&gt;transactions&lt;/em&gt; on a network of untrusted nodes. Such transactions are usually small blocks of text that say "wallet A moved X Bitcoins to wallet B at time T, and the latest transaction in the chain at time T was Z". Forget storing your GIFs on such an architecture, let alone your whole website or your self-made movie - it'd be like storing a Blueray DVD on floppy disks. Remember that it takes the average node on the network about 100 GB of storage investment to store one year of transactions, given the current volumes of Bitcoin transactions and assuming an average block size of 1 KB. Storing all kind of media data from every user on top of this would simply be unacceptable.&lt;/p&gt;

&lt;p&gt;So, if the data isn't stored on the Blockchain directly (outside of a wallet's metadata and its transactions), where is it stored?&lt;/p&gt;

&lt;p&gt;Well, once you scrape the crypto-facade the dear ol' web appears. Take the example of an NFT. The minted token contains a metadata section that references a URL - the linked digital asset. By buying the token, in theory you buy the ownership of the underlying asset. And, since this transaction occurs on the Blockchain, everybody can confirm that you are the actual owner of the asset.&lt;/p&gt;

&lt;p&gt;But wait - the digital asset is actually referenced &lt;em&gt;by URL&lt;/em&gt;, it's not actually stored on the Blockchain. What this means, in reality, is that most of the NFTs currently on sale point to web servers that run an Apache instance and expose static files.&lt;/p&gt;

&lt;p&gt;What happens if the owner of the domain stops paying for the domain name? Or for the web hosting? Or for the SSL certificate? What if the machine goes down? What if the web server is compromised and all the static assets are either scrambled by ransomware or replaced with shit emojis, or the file is simply removed from the server? If instead it's stored in a Dropbox/Google Drive folder, what happens if that account gets deleted/blocked? If the URL is publicly accessible, what prevents me from simply copying the file to my own web server, put it on sale for $0.01, buy it and say that I'm the actual owner? If the underlying physical file has the same content, and URLs are used as identifiers of digital assets, who is supposed to establish what URL is valid if two of them point to files with the same content?&lt;/p&gt;

&lt;p&gt;Another guy had similar concerns recently, so he &lt;a href="https://moxie.org/2022/01/07/web3-first-impressions.html"&gt;created a piece of digital art&lt;/a&gt; and he put it on sale via NFT.&lt;/p&gt;

&lt;p&gt;Remember: an NFT simply points to a URL. A URL simply points to a web server.  It's quite easy to embed some pieces of code into your images (for example, using GD in PHP) so that the image is rendered differently depending on the context of the HTTP request. For example, the underlying image would render in different ways on different NFT marketplaces, and, if visualized within the context of a wallet (i.e. a user that purchased the underlying asset), it would simply render a "shit" emoji. His point was quite simple: if an NFT simply points to a URL that points to a web server that any dude can run, what prevents all the digital assets purchased via NFTs from turning into a 404 error page (or "shit" emojis) at the moment of purchase or any later moment?&lt;/p&gt;

&lt;p&gt;Again, the Web 3.0 tries to address a real problem here: how to compensate digital creators for their work, while cutting away all the middlemen that may eat large shares of the profits. I've been very sensitive myself on this topic myself for a long time. But, again, it proposes the wrong solution. Middlemen aren't really cut out (again, a Blockchain isn't really decentralized), fees aren't really slashed (again, it depends on the demand/supply mechanisms that regulate the mining industry), and having digital content paid thousands of dollars living on some dude's Apache server or Wordpress blog wasn't exactly the solution to the problem of "digital ownership" that I had in mind. I mean, come on, even a naive solution like referencing the SHA-1 hash of the underlying file would have been better than this grotesque sham.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nobody can touch your digital wallets, transactions and assets - unless they can?
&lt;/h3&gt;

&lt;p&gt;Another argument often proposed by the crypto-web enthusiasts is that, once something is on the Blockchain, it's forever. There's no censorship, no risk for later data manipulation, nobody can "cancel" you.&lt;/p&gt;

&lt;p&gt;Remember the "shit emoji" dude from the previous paragraph? Well, his digital asset was later removed from OpenSea (one of the main NFT marketplaces). It is quite likely that somebody complained about the "polymorphic" nature of the image:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2uF8XUTR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://moxie.org/blog/images/nft-removed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2uF8XUTR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://moxie.org/blog/images/nft-removed.png" alt="ops" width="362" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not only: it also disappeared from the wallets of those who had legitimately purchased it.&lt;/p&gt;

&lt;p&gt;How is this possible? Well, it's quite easy to understand once we keep the previous observations in mind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An NFT is simply a token that uniquely points to a URL;&lt;/li&gt;
&lt;li&gt;Access to the Ethereum Blockchain is actually already quite centralized.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And, since wallets and transactions on the Blockchain are actually linked to users and accounts hosted on a normal website, nothing prevents the owner of the website from doing what the owner of any website can do - included blocking/banning users, modifying database records, and so on.&lt;/p&gt;

&lt;p&gt;Not only: if Blockchains are really so secure, how come do we read so much about thefts of tokens or whole wallets? Well, while the underlying Blockchains are often quite secure (rogue nodes may occasionally still be able to push illegitimate transactions, but such events are very rare), remember that digital wallets are still associated to a user on a normal web site that logs in like any other user would log into a normal web site - with username and password.  Remember that the strenght of a chain always equals the strength of its weakest link. Having a standard web layer wrapped around a Blockchain doesn't really improve security.&lt;/p&gt;

&lt;p&gt;Not only: another promise of Blockchains is that of anonymity. You are identified on the Blockchain by your wallet, and your wallet is just a pair of randomly generated cryptographic keys. But if access to the Blockchain is centralized, then you'll probably end up managing multiple wallets with a small set of service providers.  These providers can easily link multiple wallets that you manage on their website to your account, therefore enabling all kind of user analytics already performed by today's platforms.&lt;/p&gt;

&lt;p&gt;A Blockchain is a distributed ledger, but there's no point in investing the effort in building a distributed ledger if it's wrapped by a centralized layer that doesn't offer significant gains in terms of data control, security and anonymity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;The vision of a crypto-based Web 3.0 revolves around promises of greater decentralization, scalability, privacy, security, data control, transparency, and payments that cut the middlemen. It turns out that, at the current state, it cannot deliver any of these promises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Decentralization&lt;/strong&gt;: if the cost to join the network is a node is artificially high, either in terms of energy/hardware requirements (PoW) or initial financial investment (PoS), then we are in a situation where the entry barriers are higher, not lower, than running your own web site on your own web server today.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: if the entry barriers are high in the first place, and they are designed to structurally increase as the network grows, then the network can hardly scale up. If we talk of a system that consumes the whole energy demand of a country to handle 300k transactions per day, and where all the nodes are supposed to keep a synchronized copy of the whole dataset, then we are talking of a system that performs much worse, not better, than the systems that we currently have.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;: security is always as strong as the weakest link in the chain.  If you have an underlying data structure that is hard to tamper with, but access to that data structure is managed through a username/password login on a normal web site or app, then you can't overall expect the security level to go up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Privacy, transparency and data control&lt;/strong&gt;: all the interactions with the major Blockchain-based web solutions are processed by a small number of players in the industry. Consolidation in this space is already strong.  Privacy, transparency and data control are as good as when you delegate complete control of your data to a third-party on the Web. You are trusting third-parties to authenticate you, to process your online payments or to keep track of the assets that you own. What they return on their API responses is also the ultimate source of truth. The Web 3.0 basically is attempting to shift power from a tech oligarchy to another - presumably composed of those who have invested a lot into cryptocurrencies and are just searching for problems that Blockchains can solve while making them rich.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transaction fees&lt;/strong&gt;: fees are actually linked by design to the demand/supply balance of mining resources. This makes them much more volatile than the fees you would pay for a normal credit card transaction. Not only: they are also higher on average, since the cost of adding a new block to a distributed ledger through shared consensus is higher than the cost of handling the same transaction on a centralized ledger. This makes the solutions proposed by the Web 3.0 unfeasible to handle problems such as micro-payments or royalties.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I still believe that the Blockchain can be a promising technology, but, after this long hype phase, I believe that expectations about what it can actually deliver will be downsized. I don't believe that it can be used as a system for shared consensus in the first place. The original approach, the proof-of-work, is a scalability disaster by design - it was a good proof-of-concept that recycled an idea from some late-90s spam mitigation protocol, nothing more. The newly proposed approach, the proof-of-stake, is more promising, but it hasn't yet been tested enough on large scale and it hasn't yet addressed some security and consistency issues. And, even if it's successful, it's probably doomed to converge towards centralization, since the initial stakes for being a part of the network are likely to increase either with the size of the network or with the value of the underlying cryptocurrency. In short, the Blockchain proposed itself a solution to the problem of distributed consensus, but, 12 years down the line, it hasn't yet figured out how to achieve consensus in a way that is scalable, secure and distributed at the same time.&lt;/p&gt;

&lt;p&gt;I still believe that Blockchains will have a place in cutting the middlemen when it comes to B2B financial transactions, or creating financial instruments that currently require fat contracts signed by fat notaries with fat fees. There is quite an ironic twist here: Bitcoin was initially proposed in the aftermath of the 2008-2009 financial crisis. It was supposed to punish the financial system that had caused the crisis (and, in particular, the underlying level of unregulated financial speculation) by creating a new distributed financial system. Instead, smart contracts may end up powering a revolution in the very financial instruments that cryptocurrencies were supposed to destroy.&lt;/p&gt;

&lt;p&gt;Blockchains may also revolutionarise logistics: shipping an item from one side of the world to the other currently requires integrating data from many different sources, spanning over different countries and subject to different regulations. If everybody agrees to write both their transactions and their business logic to the same distributed ledger, it would solve a lot of issues in a structural way.&lt;/p&gt;

&lt;p&gt;But these use-cases probably depict very different scenarios from the ones originally envisioned.  This will be a distributed ledger managed through some proof-of-stake-like mechanism between businesses or financial institutions that already have some ways of identifying their identities - once you relax the constraints of trust enforcement and decentralization, Blockchains can actually be interesting technological tools.&lt;/p&gt;

&lt;p&gt;They might also be used to run whole national currencies. China is already working of it. This is another use-case that makes totally sense: if you own a digital wallet directly with your own central bank, then the central bank has much more direct visibility and control over its monetary policies, and it doesn't have to rely on the middlemen (e.g. retail banks) to manage the money of citizens. However, this is, again, a far cry from the Blockchain's original idea. It was initially conceived as a system to let citizens take back control of their money. It may probably end up cutting retail banks out, but only to further centralize control in the hands of the government or the central bank.&lt;/p&gt;

&lt;p&gt;However, I also believe that the crypto-based Web 3.0 addresses real problems that are still searching for solutions. Centralization of identity management and data management, and in general an increasing consolidation of power in the hands of a half a dozen of companies, are still real problems. Affordable, easy-to-setup cross-border micro-payments are still looking for a solution. The problem of handling and enforcing ownership of digital assets, and rewarding digital creators for their work, is still looking for a solution. I just believe that the Blockchain is the wrong solution for this problem, because - as many already said - it's a solution still looking for a problem to solve.&lt;/p&gt;

&lt;p&gt;In order to address the issues that caused the consolidation of technological power that plagues the Web 2.0, we must first analyse what kind of phenomena lead to technological consolidation in the first place. Once we acknowledge that the level of consolidation is proportional to the entry barriers required to compete in the market, then we may want to design systems that lower those barriers, not systems that increase them.&lt;/p&gt;

&lt;p&gt;I also believe that there is a lot of untapped potential in the previous incarnations of the Web 3.0 - both the &lt;em&gt;semantic web&lt;/em&gt; and the &lt;em&gt;web-of-things&lt;/em&gt;.  Sure, they had to face a reality where it's easier to build a platform with your own set of rules rather than getting everybody to agree on the same shared set of protocols and standards, and with an industry that has a strong bias towards centralization and de-facto standards anyways. But at least they proposed incremental solutions built on top of existing standards that address real issues in building a distributed Web - data and protocols fragmentation in the first place.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Build your self-hosted Evernote
</title>
      <dc:creator>Fabio Manganiello</dc:creator>
      <pubDate>Thu, 06 Jan 2022 19:58:18 +0000</pubDate>
      <link>https://forem.com/blacklight/build-your-self-hosted-evernote-of7</link>
      <guid>https://forem.com/blacklight/build-your-self-hosted-evernote-of7</guid>
      <description>&lt;h2&gt;
  
  
  The need for an online &lt;em&gt;second brain&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;When &lt;a href="https://evernote.com" rel="noopener noreferrer"&gt;Evernote&lt;/a&gt; launched the idea of an online notebook as a sort of "second brain" more than a decade ago, it resonated so much with what I had been trying to achieve for a while. By then I already had tons of bookmarks, text files with read-it-later links, notes I had taken across multiple devices, sketches I had taken on physical paper and drafts of articles or papers I was working on. All of this content used to be sparse across many devices, it was painful to sync, and then Evernote came like water in a desert.&lt;/p&gt;

&lt;p&gt;I have been a happy Evernote user until ~5-6 years ago, when I realized that the company had run out of ideas, and I could no longer compromise with its decisions. If Evernote was supposed to be my second brain then it should have been very simple to synchronize it with my filesystem and across multiple devices, but that wasn't as simple as it sounds. Evernote had a primitive API, a primitive web clipper, no Linux client, and, as it tried harder and harder to monetize its product, it put more and more features behind expensive tiers. Moreover, Evernote experienced &lt;a href="https://www.cnet.com/news/thousands-of-evernote-users-affected-by-data-loss/" rel="noopener noreferrer"&gt;data losses&lt;/a&gt;, &lt;a href="https://thenextweb.com/insider/2013/03/05/after-major-data-breach-evernote-accelerates-plans-to-implement-two-factor-authentication/" rel="noopener noreferrer"&gt;security breaches&lt;/a&gt; and &lt;a href="https://www.forbes.com/sites/thomasbrewster/2016/12/14/worst-privacy-policy-evernote/#525cc6c71977" rel="noopener noreferrer"&gt;privacy controversies&lt;/a&gt; that in my eyes made it unfit to handle something as precious as the notes from my life and my work. I could not compromise with a product that would charge me $5 more a month just to have it running on an additional device, especially when the product itself didn't look that solid to me. If Evernote was supposed to be my second brain then I should have been able to take it with me wherever I wanted, without having to worry on how many devices I was using it already, without having to fear future changes or more aggressive monetization policies that could have limited my ability to use the product.&lt;/p&gt;

&lt;p&gt;So I started my journey as a wanderer of note-taking and link-saving services. Yes, ideally I want something that can do both: your digital brain consists both of the notes you've taken and the links you've saved.&lt;/p&gt;

&lt;p&gt;I've tried many of them over the following years (Instapaper, Pocket, Readability, Mercury Reader, SpringPad, Google Keep, OneNote, Dropbox Paper...), but eventually got dissatisfied by most of them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In most of the cases those products fall into the note-taking category or web scraper/saver category, rarely both.&lt;/li&gt;
&lt;li&gt;In most of the cases you have to pay a monthly/yearly fee for something as simple as storing and syncing text.&lt;/li&gt;
&lt;li&gt;Many of the products above either lack an API to programmatically import/export/read data, or they put their APIs behind some premium tiers. This is a no-go for me: if the company that builds the product goes down, the last thing I want is my personal notes, links and bookmarks to go down with it with no easy way to get them out.&lt;/li&gt;
&lt;li&gt;Most of those products don't have local filesystem sync features: everything only works in their app.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My dissatisfaction with the products on the market was a bit relieved when I discovered &lt;a href="https://obsidian.md/" rel="noopener noreferrer"&gt;Obsidian&lt;/a&gt;. A Markdown-based, modern-looking, multi-device product that transparently stores your notes on your own local storage, and it even provides plenty of community plugins? That covers all I want, it's almost too good to be true! And, indeed, it is too good to be true. Obsidian &lt;a href="https://obsidian.md/pricing" rel="noopener noreferrer"&gt;charges&lt;/a&gt; $8 a month just for syncing content across devices (copying content to their own cloud), and $16 a month if you want to publish/share your content. Those are unacceptably high prices for something as simple as synchronizing and sharing text files! This was the trigger that motivated me to take the matter into my own hands, so I came up with the wishlist for my ideal "second brain" app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It needs to be self-hosted. No cloud services involved: it's easy to put stuff on somebody else's cloud, it's usually much harder to take it out, and cloud services are unreliable by definition - they may decide from a moment to another that they aren't making enough money, charge more for some features you are using, while keeping your own most precious data as hostage. Or, worse, they could go down and take all of your data with them.&lt;/li&gt;
&lt;li&gt;Each device should have a local copy of my notebook, and it should be simple to synchronize changes across these copies.&lt;/li&gt;
&lt;li&gt;It ought to be Markdown-based. Markdown is portable, clean, easy to index and search, it can easily be converted to HTML if required, but it's much less cumbersome to read and write, and it's easy to import/export. To give an idea of the underestimated power and flexibility of Markdown, keep in mind that all the articles on &lt;a href="https://blog.platypush.tech" rel="noopener noreferrer"&gt;the Platypush blog&lt;/a&gt; are static Markdown files on a local server that are converted on the fly to HTML before being served to your browser.&lt;/li&gt;
&lt;li&gt;It needs to be able to handle my own notes, as well as parse and convert to Markdown web pages that I'd like to save or read later.&lt;/li&gt;
&lt;li&gt;It must be easy to add and modify content. Whether I want to add a new link from my browser session on my laptop, phone or tablet, or type some text on the fly from my phone, or resume working on a draft from another device, I should be able to do so with no friction, as if I were working always on the same device.&lt;/li&gt;
&lt;li&gt;It needs to work offline. I want to be able to work on a blog article while I'm on a flight with no Internet connection, and I expect the content to be automatically synced as soon as my device gets a connection.&lt;/li&gt;
&lt;li&gt;It needs to be file-based. I'm sick of custom formats, arcane APIs and other barriers and pointless abstractions between me and my text. The KISS rule applies here: if it's a text file, and it appears on my machine inside a normal directory, then expose it as a text file, and you'll get primitives such as read/create/modify/copy/move/delete for free.&lt;/li&gt;
&lt;li&gt;It needs to encapsulate some good web scraping/parsing logic, so every web page can be distilled into a readable and easily exportable Markdown format.&lt;/li&gt;
&lt;li&gt;It needs to allow automated routines - for instance, automatically fetch new content from an RSS feed and download it in readable format on the shared repository.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It looks like a long shopping list, but it actually doesn't take that much to implement it. It's time to get to the whiteboard and design its architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  High-level architecture
&lt;/h2&gt;

&lt;p&gt;From a high-level perspective, the architecture we are trying to build resembles something like this:&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%2Fblog.platypush.tech%2Fimg%2Fself-hosted-notebook-architecture.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%2Fblog.platypush.tech%2Fimg%2Fself-hosted-notebook-architecture.png" alt="High-level architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The git repository
&lt;/h2&gt;

&lt;p&gt;We basically use a git server as the repository for our notes and links. It could be a private repo on GitHub or Gitlab, or even a static folder initialized as a git repo on a server accessible over SSH. There are many advantages in choosing a versioning system like git as the source of truth for your notebook content:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;History tracking&lt;/em&gt; comes for free: it's easy to keep track of changes commit by different devices, as well as rollback to previous versions - nothing is ever really lost.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Easy synchronization&lt;/em&gt;: pushing new content to your notes can be mapped to a &lt;code&gt;git push&lt;/code&gt;, synchronizing new content on other devices can be mapped to a &lt;code&gt;git pull&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Native Markdown-friendly interfaces&lt;/em&gt;: both GitHub and Gitlab provide native good interfaces to visualize Markdown content. Browsing and managing your notebook is as easy as browsing a git repo.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Easy to import and export&lt;/em&gt;: exporting your notebook to another device is as simple as running a &lt;code&gt;git clone&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Storage flexibility&lt;/em&gt;: you can create the repo on a cloud instance, on a self-hosted instance, or on any machine with an SSH interface. The repo can live anywhere, as long as it is accessible to the devices that you want to use.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the first requirement for this project is to set up a git repository on whatever source you want to use a central storage for your notebook. We have mainly three options for this:&lt;/p&gt;

&lt;h4&gt;
  
  
  Create a new repo on a GitHub/Gitlab cloud instance.
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Pros&lt;/em&gt;: you don't have to maintain a git server, you just have to create a new project, and you have all the fancy interfaces for managing files and viewing Markdown content.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Cons&lt;/em&gt;: it's not really 100% self-hosted, isn't it? :)&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Host a Gitlab instance yourself.
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Pros&lt;/em&gt;: plenty of flexibility when it comes to hosting. You can even run the server on a machine only accessible from the outside over a VPN, which brings some nice security features and content encapsulation. Plus, you have a modern interface like Gitlab to handle your files, and you can also easily set up repository automation through web hooks.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Cons&lt;/em&gt;: installing and running a Gitlab instance is a process with its own learning curve. Plus, a Gitlab instance is usually quite resource-hungry - don't run it on a Raspberry Pi if you want the user experience to be smooth.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Initialize an empty repository on any publicly accessible server (or accessible over VPN) with an SSH interface.
&lt;/h4&gt;

&lt;p&gt;An often forgotten feature of git is that it's basically a wrapper on top of SSH, therefore you can create a repo on the fly on any machine that runs an SSH server - no need for a full-blown web framework on top of it. It's as simple as:&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="c"&gt;# Server machine&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/user/notebook.git
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /home/user/notebook.git
&lt;span class="nv"&gt;$ &lt;/span&gt;git init &lt;span class="nt"&gt;--bare&lt;/span&gt;

&lt;span class="c"&gt;# Client machine&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git clone user@remote-machine:/home/user/notebook.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Pros&lt;/em&gt;: the most flexible option: you can run your notebook storage on literally anything that has a CPU, an SSH interface and git.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Cons&lt;/em&gt;: you won't have a fancy native interface to manage your files, nor repository automation features such as actions or web hooks (available with GitHub and Gitlab respectively).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Markdown web server
&lt;/h2&gt;

&lt;p&gt;It may be handy to have a web server to access your notes and links from any browser, especially if your repository doesn't live on GitHub/Gitlab, and therefore it doesn't have a native way to expose the files over the web.&lt;/p&gt;

&lt;p&gt;Clone the notebook repo on the machine where you want to expose the Markdown web server and then install &lt;a href="https://github.com/DannyBen/madness" rel="noopener noreferrer"&gt;Madness&lt;/a&gt; and its dependencies:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ruby-full
&lt;span class="nv"&gt;$ &lt;/span&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;madness
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take note of where the &lt;code&gt;madness&lt;/code&gt; executable was installed and create a new user systemd service file under &lt;code&gt;~/.config/systemd/user/madness.service&lt;/code&gt; to manage the server on your repo folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Serve Markdown content over HTML&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/user/.gem/ruby/version/bin/madness /path/to/the/notebook --port 9999&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the systemd daemon and start/enable the server:&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="nv"&gt;$ &lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start madness
&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;madness
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything went well you can head your browser to &lt;code&gt;http://host:9999&lt;/code&gt; and you should see the Madness interface with your Markdown files.&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%2Fblog.platypush.tech%2Fimg%2Fmadness-screenshot.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%2Fblog.platypush.tech%2Fimg%2Fmadness-screenshot.png" alt="Madness interface screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can easily configure a &lt;a href="https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/" rel="noopener noreferrer"&gt;nginx reverse proxy&lt;/a&gt; or an &lt;a href="https://www.ssh.com/academy/ssh/tunneling" rel="noopener noreferrer"&gt;SSH tunnel&lt;/a&gt; to expose the server outside of the local network.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MQTT broker
&lt;/h2&gt;

&lt;p&gt;An MQTT broker is another crucial ingredient in this set up. It is used to asynchronously transmit events such as a request to add a new URL or update the local repository copies.&lt;/p&gt;

&lt;p&gt;Any of the open-source MQTT brokers out there should do the job. I personally use &lt;a href="https://mosquitto.org/" rel="noopener noreferrer"&gt;Mosquitto&lt;/a&gt; for most of my projects, but &lt;a href="https://www.rabbitmq.com/" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt;, &lt;a href="https://github.com/moscajs/aedes" rel="noopener noreferrer"&gt;Aedes&lt;/a&gt; or any other broker should all just work.&lt;/p&gt;

&lt;p&gt;Just like the git server, you should also install the MQTT on a machine that is either publicly accessible, or it is accessible over VPN by all the devices you want to use your notebook on. If you opt for a machine with a publicly accessible IP address then it's advised to enable both SSL and username/password authentication on your broker, so unauthorized parties won't be able to connect to it.&lt;/p&gt;

&lt;p&gt;Taking the case of Mosquitto, the installation and configuration is pretty straightforward. Install the &lt;code&gt;mosquitto&lt;/code&gt; package from your favourite package manager, the installation process should also create a configuration file under &lt;code&gt;/etc/mosquitto/mosquitto.conf&lt;/code&gt;. In the case of an SSL configuration with username and password, you would usually configure the following options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Usually 1883 for non-SSL connections, 8883 for SSL connections
&lt;/span&gt;&lt;span class="err"&gt;port&lt;/span&gt; &lt;span class="err"&gt;8883&lt;/span&gt;

&lt;span class="c"&gt;# SSL/TLS version
&lt;/span&gt;&lt;span class="err"&gt;tls_version&lt;/span&gt; &lt;span class="err"&gt;tlsv1.2&lt;/span&gt;

&lt;span class="c"&gt;# Path to the certificate chain
&lt;/span&gt;&lt;span class="err"&gt;cafile&lt;/span&gt; &lt;span class="err"&gt;/etc/mosquitto/certs/chain.crt&lt;/span&gt;

&lt;span class="c"&gt;# Path to the server certificate
&lt;/span&gt;&lt;span class="err"&gt;certfile&lt;/span&gt; &lt;span class="err"&gt;/etc/mosquitto/certs/server.crt&lt;/span&gt;

&lt;span class="c"&gt;# Path to the server private key
&lt;/span&gt;&lt;span class="err"&gt;keyfile&lt;/span&gt; &lt;span class="err"&gt;/etc/mosquitto/certs/server.key&lt;/span&gt;

&lt;span class="c"&gt;# Set to false to disable access without username and password
&lt;/span&gt;&lt;span class="err"&gt;allow_anonymous&lt;/span&gt; &lt;span class="err"&gt;false&lt;/span&gt;

&lt;span class="c"&gt;# Password file, which contains username:password pairs
# You can create and manage a password file by following the
# instructions reported here:
# https://mosquitto.org/documentation/authentication-methods/
&lt;/span&gt;&lt;span class="err"&gt;password_file&lt;/span&gt; &lt;span class="err"&gt;/etc/mosquitto/passwords.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't need SSL encryption and authentication on your broker (which is ok if you are running the broker on a private network and accessing it from the outside over VPN) then you'll only need to set the &lt;code&gt;port&lt;/code&gt; option.&lt;/p&gt;

&lt;p&gt;After you have configured the MQTT broker, you can start it and enable it via &lt;code&gt;systemd&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;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start mosquitto
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;mosquitto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then use an MQTT client like &lt;a href="http://mqtt-explorer.com/" rel="noopener noreferrer"&gt;MQTT Explorer&lt;/a&gt; to connect to the broker and verify that everything is working.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Platypush automation
&lt;/h2&gt;

&lt;p&gt;Once the git repo and the MQTT broker are in place, it's time to set up Platypush on one of the machines where you want to keep your notebook synchronized - e.g. your laptop.&lt;/p&gt;

&lt;p&gt;In this context, Platypush is used to glue together the pieces of the sync automation by defining the following chains of events:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a file system change is detected in the folder where the notebook is cloned (for example because a note was added, removed or edited), start a timer than within e.g. 30 seconds synchronizes the changes to the git repository (the timer is used to throttle the frequency of update events). Then send a message to the MQTT &lt;code&gt;notebook/sync&lt;/code&gt; topic to tell the other clients that they should synchronize their copies of the repository.&lt;/li&gt;
&lt;li&gt;When a client receives a message on &lt;code&gt;notebook/sync&lt;/code&gt;, and the originator is different from the client itself (this is necessary in order to prevent "sync loops"), pull the latest changes from the remote repository.&lt;/li&gt;
&lt;li&gt;When a specific client (which will be in charge of scraping URLs and adding new remote content) receives a message on the MQTT &lt;code&gt;notebook/save&lt;/code&gt; topic with a URL attached, the content of the associated web page will be parsed and saved to the notebook ("Save URL" feature).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same automation logic can be set up on as many clients as you like.&lt;/p&gt;

&lt;p&gt;The first step is to install the Redis server and Platypush on your client machine. For example, on a Debian-based system:&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="c"&gt;# Install Redis&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;redis-server
&lt;span class="c"&gt;# Start and enable the Redis server&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start redis-server
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;redis-server
&lt;span class="c"&gt;# Install Platypush&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;platypush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll then have to create a configuration file to tell Platypush which services you want to use. Our use-case will require the following integrations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mqtt&lt;/code&gt; (&lt;a href="https://docs.platypush.tech/platypush/backend/mqtt.html" rel="noopener noreferrer"&gt;backend&lt;/a&gt; and &lt;a href="https://docs.platypush.tech/platypush/plugins/mqtt.html" rel="noopener noreferrer"&gt;plugin&lt;/a&gt;), used to subscribe to sync/save topics and dispatch messages to the broker.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.platypush.tech/platypush/backend/file.monitor.html" rel="noopener noreferrer"&gt;&lt;code&gt;file.monitor&lt;/code&gt; backend&lt;/a&gt;, used to monitor changes to local folders.&lt;/li&gt;
&lt;li&gt;[Optional] &lt;a href="https://docs.platypush.tech/platypush/plugins/pushbullet.html" rel="noopener noreferrer"&gt;&lt;code&gt;pushbullet&lt;/code&gt;&lt;/a&gt;, or an alternative way to deliver notifications to other devices (such as &lt;a href="https://docs.platypush.tech/platypush/plugins/chat.telegram.html" rel="noopener noreferrer"&gt;&lt;code&gt;telegram&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://docs.platypush.tech/platypush/plugins/twilio.html" rel="noopener noreferrer"&gt;&lt;code&gt;twilio&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://docs.platypush.tech/platypush/plugins/gotify.html" rel="noopener noreferrer"&gt;&lt;code&gt;gotify&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://docs.platypush.tech/platypush/plugins/mailgun.html" rel="noopener noreferrer"&gt;&lt;code&gt;mailgun&lt;/code&gt;&lt;/a&gt;). We'll use this to notify other clients when new content has been added.&lt;/li&gt;
&lt;li&gt;[Optional] the &lt;a href="https://docs.platypush.tech/platypush/plugins/http.webpage.html" rel="noopener noreferrer"&gt;&lt;code&gt;http.webpage&lt;/code&gt;&lt;/a&gt; integration, used to scrape a web page's content to Markdown or PDF.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start by creating a &lt;code&gt;config.yaml&lt;/code&gt; file with your integrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The name of your client&lt;/span&gt;
&lt;span class="na"&gt;device_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-client&lt;/span&gt;

&lt;span class="na"&gt;mqtt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-mqtt-server&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1883&lt;/span&gt;
  &lt;span class="c1"&gt;# Uncomment the lines below for SSL/user+password authentication&lt;/span&gt;
  &lt;span class="c1"&gt;# port: 8883&lt;/span&gt;
  &lt;span class="c1"&gt;# username: user&lt;/span&gt;
  &lt;span class="c1"&gt;# password: pass&lt;/span&gt;
  &lt;span class="c1"&gt;# tls_cafile: ~/path/to/ssl.crt&lt;/span&gt;
  &lt;span class="c1"&gt;# tls_version: tlsv1.2&lt;/span&gt;

&lt;span class="c1"&gt;# Specify the topics you want to subscribe here&lt;/span&gt;
&lt;span class="na"&gt;backend.mqtt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;listeners&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;topics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;notebook/sync&lt;/span&gt;

&lt;span class="c1"&gt;# The configuration for the file monitor follows.&lt;/span&gt;
&lt;span class="c1"&gt;# This logic triggers FileSystemEvents whenever a change&lt;/span&gt;
&lt;span class="c1"&gt;# happens on the specified folder. We can use these events&lt;/span&gt;
&lt;span class="c1"&gt;# to build our sync logic&lt;/span&gt;
&lt;span class="na"&gt;backend.file.monitor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Path to the folder where you have cloned the notebook&lt;/span&gt;
    &lt;span class="c1"&gt;# git repo on your client&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/path/to/the/notebook&lt;/span&gt;
      &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="c1"&gt;# Ignore changes on non-content sub-folders, such as .git or&lt;/span&gt;
      &lt;span class="c1"&gt;# other configuration/cache folders&lt;/span&gt;
      &lt;span class="na"&gt;ignore_directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.git&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.obsidian&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then generate a new Platypush virtual environment from the configuration file:&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="nv"&gt;$ &lt;/span&gt;platyvenv build &lt;span class="nt"&gt;-c&lt;/span&gt; config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the command has run, it should report a line like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Platypush virtual environment prepared under /home/user/.local/share/platypush/venv/my-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's call this path &lt;code&gt;$PREFIX&lt;/code&gt;. Create a structure to store your scripts under &lt;code&gt;$PREFIX/etc/platypush&lt;/code&gt; (a copy of the&lt;br&gt;
&lt;code&gt;config.yaml&lt;/code&gt; file should already be there at this point). The structure will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;$&lt;span class="n"&gt;PREFIX&lt;/span&gt;
 -&amp;gt; &lt;span class="n"&gt;etc&lt;/span&gt;
   -&amp;gt; &lt;span class="n"&gt;platypush&lt;/span&gt;
    -&amp;gt; &lt;span class="n"&gt;config&lt;/span&gt;.&lt;span class="n"&gt;yaml&lt;/span&gt;      &lt;span class="c"&gt;# Configuration file
&lt;/span&gt;    -&amp;gt; &lt;span class="n"&gt;scripts&lt;/span&gt;          &lt;span class="c"&gt;# Scripts folder
&lt;/span&gt;      -&amp;gt; &lt;span class="err"&gt;__&lt;/span&gt;&lt;span class="n"&gt;init__&lt;/span&gt;.&lt;span class="n"&gt;py&lt;/span&gt;    &lt;span class="c"&gt;# Empty file
&lt;/span&gt;      -&amp;gt; &lt;span class="n"&gt;notebook&lt;/span&gt;.&lt;span class="n"&gt;py&lt;/span&gt;    &lt;span class="c"&gt;# Logic for notebook synchronization
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's proceed with defining the core logic in &lt;code&gt;notebook.py&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RLock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Timer&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.event.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.message.event.file&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FileSystemEvent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.message.event.mqtt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MQTTMessageEvent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.procedure&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;procedure&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notebook&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;repo_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/path/to/your/git/repo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="n"&gt;sync_timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;sync_timer_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RLock&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;should_sync_notebook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MQTTMessageEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Only synchronize the notebook if a sync request came from
    a source other than ourselves - this is required to prevent
    &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sync loops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, where a client receives its own sync message
    and broadcasts sync requests again and again.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;device_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;origin&lt;/span&gt;&lt;span class="sh"&gt;'&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;cancel_sync_timer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Utility function to cancel a pending synchronization timer.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;sync_timer&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sync_timer_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sync_timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;sync_timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;sync_timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reset_sync_timer&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Utility function to start a synchronization timer.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;sync_timer&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sync_timer_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;cancel_sync_timer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;sync_timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sync_notebook&lt;/span&gt;&lt;span class="p"&gt;,&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;sync_timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="nd"&gt;@hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MQTTMessageEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notebook/sync&lt;/span&gt;&lt;span class="sh"&gt;'&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;on_notebook_remote_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    This hook is triggered when a message is received on the
    notebook/sync MQTT topic. It triggers a sync between the
    local and remote copies of the repository.
    &lt;/span&gt;&lt;span class="sh"&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="nf"&gt;should_sync_notebook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="nf"&gt;sync_notebook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FileSystemEvent&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;on_notebook_local_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    This hook is triggered when a change (i.e. file/directory
    create/update/delete) is performed on the folder where the
    repository is cloned. It starts a timer to synchronize the
    local and remote repository copies.
    &lt;/span&gt;&lt;span class="sh"&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;event&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="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_path&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Synchronizing repo path &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;reset_sync_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@procedure&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync_notebook&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    This function holds the main synchronization logic.
    It is declared through the @procedure decorator, so you can also
    programmatically call it from your requests through e.g.
    `procedure.notebook.sync_notebook`.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# The timer lock ensures that only one thread at the time can
&lt;/span&gt;    &lt;span class="c1"&gt;# synchronize the notebook
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sync_timer_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Cancel any previously awaiting timer
&lt;/span&gt;        &lt;span class="nf"&gt;cancel_sync_timer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Synchronizing notebook - path: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getcwd&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="nf"&gt;chdir&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;has_stashed_changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Check if the local copy of the repo has changes
&lt;/span&gt;            &lt;span class="n"&gt;git_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git status --porcelain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;git_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;The local copy has changes: synchronizing them to the repo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="c1"&gt;# If we have modified/deleted files then we stash the local changes
&lt;/span&gt;                &lt;span class="c1"&gt;# before pulling the remote changes to prevent conflicts
&lt;/span&gt;                &lt;span class="n"&gt;has_modifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^\s*[MD]\s+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&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;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;git_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_modifications&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git stash&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignore_errors&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;has_stashed_changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

                &lt;span class="c1"&gt;# Pull the latest changes from the repo
&lt;/span&gt;                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git pull --rebase&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_modifications&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="c1"&gt;# Un-stash the local changes
&lt;/span&gt;                    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git stash pop&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

                &lt;span class="c1"&gt;# Add, commit and push the local changes
&lt;/span&gt;                &lt;span class="n"&gt;has_stashed_changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
                &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;device_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git add .&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git commit -a -m &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Automatic sync triggered by &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git push origin main&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

                &lt;span class="c1"&gt;# Notify other clients by pushing a message to the notebook/sync topic
&lt;/span&gt;                &lt;span class="c1"&gt;# having this client ID as the origin. As an alternative, if you are using
&lt;/span&gt;                &lt;span class="c1"&gt;# Gitlab to host your repo, you can also configure a webhook that is called
&lt;/span&gt;                &lt;span class="c1"&gt;# upon push events and sends the same message to notebook/sync.
&lt;/span&gt;                &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mqtt.publish&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notebook/sync&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;origin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;device_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;# If we have no local changes, just pull the remote changes
&lt;/span&gt;                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git pull&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_stashed_changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git stash pop&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

            &lt;span class="c1"&gt;# In case of errors, retry in 5 minutes
&lt;/span&gt;            &lt;span class="nf"&gt;reset_sync_timer&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;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="k"&gt;finally&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="nf"&gt;chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Notebook synchronized&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can start the newly configured environment:&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="nv"&gt;$ &lt;/span&gt;platyvenv start my-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or create a systemd user service for it under &lt;code&gt;~/.config/systemd/user/platypush-notebook.service&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;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; &amp;gt; ~/.config/systemd/user/platypush-notebook.service
[Unit]
Description=Platypush notebook automation
After=network.target

[Service]
ExecStart=/path/to/platyvenv start my-client
ExecStop=/path/to/platyvenv stop my-client
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start platypush-notebook
&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;platypush-notebook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the service is running, try and create a new Markdown file under the monitored repository local copy. Within a few seconds the automation should be triggered and the new file should be automatically pushed to the repo. If you are running the code on multiple hosts, then those should also fetch the updates within seconds. You can also run an instance on the same server that runs Madness to synchronize its copy of the repo, and your web instance will remain in sync with any updates. Congratulations, you have set up a distributed network to synchronize your notes!&lt;/p&gt;

&lt;h2&gt;
  
  
  Android setup
&lt;/h2&gt;

&lt;p&gt;You may probably want a way to access your notebook also on your phone and tablet, and keep the copy on your mobile devices automatically in sync with the server.&lt;/p&gt;

&lt;p&gt;Luckily, it is possible to install and run Platypush on Android through &lt;a href="https://termux.com/" rel="noopener noreferrer"&gt;&lt;code&gt;Termux&lt;/code&gt;&lt;/a&gt;, and the logic you have set up on your laptops and servers should also work flawlessly on Android. Termux allows you to run a Linux environment in user mode with no need for rooting your device.&lt;/p&gt;

&lt;p&gt;First, install the &lt;a href="https://f-droid.org/packages/com.termux/" rel="noopener noreferrer"&gt;&lt;code&gt;Termux&lt;/code&gt; app&lt;/a&gt; on your Android device. Optionally, you may also want to install the following apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://f-droid.org/en/packages/com.termux.api/" rel="noopener noreferrer"&gt;&lt;code&gt;Termux:API&lt;/code&gt;&lt;/a&gt;: to programmatically access Android features (e.g. SMS texts, camera, GPS, battery level etc.) from your scripts.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://f-droid.org/en/packages/com.termux.boot/" rel="noopener noreferrer"&gt;&lt;code&gt;Termux:Boot&lt;/code&gt;&lt;/a&gt;: to start services such as Redis and Platypush at boot time without having to open the Termux app first (advised).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://f-droid.org/en/packages/com.termux.widget/" rel="noopener noreferrer"&gt;&lt;code&gt;Termux:Widget&lt;/code&gt;&lt;/a&gt;: to add scripts (for example to manually start Platypush or synchronize the notebook) on the home screen.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://f-droid.org/en/packages/com.termux.gui/" rel="noopener noreferrer"&gt;&lt;code&gt;Termux:GUI&lt;/code&gt;&lt;/a&gt;: to add support for visual elements (such as dialogs and widgets for sharing content) to your scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After installing Termux, open a new session, update the packages, install &lt;code&gt;termux-services&lt;/code&gt; (for services support) and enable SSH access (it's usually more handy to type commands on a physical keyboard than a phone screen):&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="nv"&gt;$ &lt;/span&gt;pkg update
&lt;span class="nv"&gt;$ &lt;/span&gt;pkg &lt;span class="nb"&gt;install &lt;/span&gt;termux-services openssh
&lt;span class="c"&gt;# Start and enable the SSH service&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;sv up sshd
&lt;span class="nv"&gt;$ &lt;/span&gt;sv-enable sshd
&lt;span class="c"&gt;# Set a user password&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A service that is enabled through &lt;code&gt;sv-enable&lt;/code&gt; will be started when a Termux session is first opened, but not at boot time unless Termux is started. If you want a service to be started a boot time, you need to install the &lt;code&gt;Termux:Boot&lt;/code&gt; app and then place the scripts you want to run at boot time inside the &lt;code&gt;~/.termux/boot&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;After starting &lt;code&gt;sshd&lt;/code&gt; and setting a password, you should be able to log in to your Android device over SSH:&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="nv"&gt;$ &lt;/span&gt;ssh &lt;span class="nt"&gt;-p&lt;/span&gt; 8022 anyuser@android-device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next step is to enable access for Termux to the internal storage (by default it can only access the app's own data folder). This can easily be done by running &lt;code&gt;termux-setup-storage&lt;/code&gt; and allowing storage access on the prompt. We may also want to disable battery optimization for Termux, so the services won't be killed in case of inactivity.&lt;/p&gt;

&lt;p&gt;Then install git, Redis, Platypush and its Python dependencies, and start/enable the Redis server:&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="nv"&gt;$ &lt;/span&gt;pkg &lt;span class="nb"&gt;install &lt;/span&gt;git redis python3
&lt;span class="nv"&gt;$ &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;platypush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If running the &lt;code&gt;redis-server&lt;/code&gt; command results in an error, then you may need to explicitly disable a warning for a COW bug for ARM64 architectures in the Redis configuration file. Simply add or uncomment the following line in &lt;code&gt;/data/data/com.termux/files/usr/etc/redis.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ignore-warnings ARM64-COW-BUG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then need to create a service for Redis, since it's not available by default. Termux doesn't use systemd to manage services, since that would require access to the PID 1, which is only available to the root user. Instead, it uses it own system of scripts that goes under the name of &lt;a href="https://wiki.termux.com/wiki/Termux-services" rel="noopener noreferrer"&gt;&lt;em&gt;Termux services&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Services are installed under &lt;code&gt;/data/data/com.termux/files/usr/var/service&lt;/code&gt;. Just &lt;code&gt;cd&lt;/code&gt; to that directory and copy the available &lt;code&gt;sshd&lt;/code&gt; service to &lt;code&gt;redis&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;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /data/data/com.termux/files/usr/var/service
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; sshd redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then replace the content of the &lt;code&gt;run&lt;/code&gt; file in the service directory with this:&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="c"&gt;#!/data/data/com.termux/files/usr/bin/sh&lt;/span&gt;
&lt;span class="nb"&gt;exec &lt;/span&gt;redis-server 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart Termux so that it refreshes its list of services, and start/enable the Redis service (or create a boot script for it):&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="nv"&gt;$ &lt;/span&gt;sv up redis
&lt;span class="nv"&gt;$ &lt;/span&gt;sv-enable redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that you can access the &lt;code&gt;/sdcard&lt;/code&gt; folder (shared storage) after restarting Termux. If that's the case, we can now clone the notebook repo under &lt;code&gt;/sdcard/notebook&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;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone git-url /sdcard/notebook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The steps for installing and configuring the Platypush automation are the same shown in the previous section, with the following exceptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;repo_path&lt;/code&gt; in the &lt;code&gt;notebook.py&lt;/code&gt; script needs to point to &lt;code&gt;/sdcard/notebook&lt;/code&gt; - if the notebook is cloned on the user's home directory then other apps won't be able to access it.&lt;/li&gt;
&lt;li&gt;If you want to run it in a service, you'll have to follow the same steps illustrated for Redis instead of creating a systemd service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may also want to redirect the Platypush stdout/stderr to a log file, since Termux messages don't have the same sophisticated level of logging provided by systemd. The startup command should therefore look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;platyvenv start my-client &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /path/to/logs/platypush.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once everything is configured and you restart Termux, Platypush should automatically start in the background - you can check the status by running a &lt;code&gt;tail&lt;/code&gt; on the log file or through the &lt;code&gt;ps&lt;/code&gt; command. If you change a file in your notebook on either your Android device or your laptop, everything should now get up to date within a minute.&lt;/p&gt;

&lt;p&gt;Finally, we can also leverage &lt;code&gt;Termux:Shortcuts&lt;/code&gt; to add a widget to the home screen to manually trigger the sync process - maybe because an update was received while the phone was off or the Platypush service was not running. Create a &lt;code&gt;~/.shortcuts&lt;/code&gt; folder with a script inside named e.g. &lt;code&gt;sync_notebook.sh&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;&lt;span class="c"&gt;#!/data/data/com.termux/files/usr/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | python
from platypush.utils import run

run('mqtt.publish', topic='notebook/sync', msg={'origin': None})
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script leverages the &lt;code&gt;platypush.utils.run&lt;/code&gt; method to send a message to the &lt;code&gt;notebook/sync&lt;/code&gt; MQTT topic with no &lt;code&gt;origin&lt;/code&gt; to force all the subscribed clients to pull the latest updates from the remote server.&lt;/p&gt;

&lt;p&gt;You can now browse to the widgets' menu of your Android device (usually it's done by long-pressing an empty area on the launcher), select &lt;em&gt;Termux shortcut&lt;/em&gt; and then select your newly created script. By clicking on the icon you will force a sync across all the connected devices.&lt;/p&gt;

&lt;p&gt;Once Termux is properly configured, you don't need to repeat the whole procedure on other Android devices. Simply use the &lt;a href="https://wiki.termux.com/wiki/Backing_up_Termux" rel="noopener noreferrer"&gt;Termux backup&lt;/a&gt; scripts to back up your whole configuration and copy it/restore it on another device, and you'll have the whole synchronization logic up and running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Obsidian app
&lt;/h2&gt;

&lt;p&gt;Now that the backend synchronization logic is in place, it's time to move to the frontend side. As mentioned earlier, Obsidian is an option I really like - it has a modern interface, it's cross-platform, it's &lt;a href="https://www.electronjs.org/" rel="noopener noreferrer"&gt;electronjs-based&lt;/a&gt;, it has many plugins, it relies on simple Markdown, and it just needs a local folder to work. As mentioned earlier, you would normally need to subscribe to Obsidian Sync in order to synchronize notes across devices, but now you've got a self-synchronizing git repo copy on any device you like. So just install Obsidian on your computer or mobile, point it to the local copy of the git notebook, and you're set to go!&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%2Fblog.platypush.tech%2Fimg%2Fobsidian-screenshot.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%2Fblog.platypush.tech%2Fimg%2Fobsidian-screenshot.png" alt="Obsidian screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The NextCloud option
&lt;/h2&gt;

&lt;p&gt;Another nice option is to synchronize your notebook across multiple devices is to use a &lt;a href="https://nextcloud.com/" rel="noopener noreferrer"&gt;NextCloud&lt;/a&gt; instance. NextCloud provides a &lt;a href="https://apps.nextcloud.com/apps/notes" rel="noopener noreferrer"&gt;Notes app&lt;/a&gt; that already supports notes in Markdown format, and it also comes with an &lt;a href="https://f-droid.org/en/packages/it.niedermann.owncloud.notes/" rel="noopener noreferrer"&gt;Android app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If that's the way you want to go, you can still have notes&amp;lt;-&amp;gt;git synchronization by simply setting up the Platypush notebook automation on the server where NextCloud is running. Just clone the repository to your NextCloud Notes folder:&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="nv"&gt;$ &lt;/span&gt;git clone git-url /path/to/nextcloud/data/user/files/Notes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then set the &lt;code&gt;repo_path&lt;/code&gt; in &lt;code&gt;notebook.py&lt;/code&gt; to this directory.&lt;/p&gt;

&lt;p&gt;Keep in mind however that local changes in the &lt;code&gt;Notes&lt;/code&gt; folder will not be synchronized to the NextCloud app until the next cron is executed. If you want the changes to be propagated as soon as they are pushed to the git repo, then you'll have to add an extra piece of logic to the script that synchronizes the notebook, in order to rescan the &lt;code&gt;Notes&lt;/code&gt; folder for changes. Also, Platypush will have to run with the same user that runs the NextCloud web server, because of the requirements for executing the &lt;code&gt;occ&lt;/code&gt; script:&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;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;

&lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notebook&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Path to the NextCloud occ script
&lt;/span&gt;&lt;span class="n"&gt;occ_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/srv/http/nextcloud/occ&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync_notebook&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="nf"&gt;refresh_nextcloud&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;refresh_nextcloud&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;php &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;occ_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; files:scan --path=/nextcloud-user/files/Notes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shell.exec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;php &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;occ_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; files:cleanup&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your notebook is now synchronized with NextCloud, and it can be accessed from any NextCloud client!&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation to parse and save web pages
&lt;/h2&gt;

&lt;p&gt;Now that we have a way to keep our notes synchronized across multiple devices and interfaces, let's explore how we can parse web pages and save them in our notebook in Markdown format - we may want to read them later on another device, read the content without all the clutter, or just keep a persistent track of the articles that we have read.&lt;/p&gt;

&lt;p&gt;Elect a notebook client to be in charge of scraping and saving URLs. This client will have a configuration like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The name of your client&lt;/span&gt;
&lt;span class="na"&gt;device_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-client&lt;/span&gt;

&lt;span class="na"&gt;mqtt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-mqtt-server&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1883&lt;/span&gt;
  &lt;span class="c1"&gt;# Uncomment the lines below for SSL/user+password authentication&lt;/span&gt;
  &lt;span class="c1"&gt;# port: 8883&lt;/span&gt;
  &lt;span class="c1"&gt;# username: user&lt;/span&gt;
  &lt;span class="c1"&gt;# password: pass&lt;/span&gt;
  &lt;span class="c1"&gt;# tls_cafile: ~/path/to/ssl.crt&lt;/span&gt;
  &lt;span class="c1"&gt;# tls_version: tlsv1.2&lt;/span&gt;

&lt;span class="c1"&gt;# Specify the topics you want to subscribe here&lt;/span&gt;
&lt;span class="na"&gt;backend.mqtt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;listeners&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;topics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;notebook/sync&lt;/span&gt;
       &lt;span class="c1"&gt;# notebook/save will be used to send parsing requests&lt;/span&gt;
       &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;notebook/save&lt;/span&gt;

&lt;span class="c1"&gt;# Monitor the local repository copy for changes&lt;/span&gt;
&lt;span class="na"&gt;backend.file.monitor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Path to the folder where you have cloned the notebook&lt;/span&gt;
    &lt;span class="c1"&gt;# git repo on your client&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/path/to/the/notebook&lt;/span&gt;
      &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="c1"&gt;# Ignore changes on non-content sub-folders, such as .git or&lt;/span&gt;
      &lt;span class="c1"&gt;# other configuration/cache folders&lt;/span&gt;
      &lt;span class="na"&gt;ignore_directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.git&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.obsidian&lt;/span&gt;

&lt;span class="c1"&gt;# Enable the http.webpage integration for parsing web pages&lt;/span&gt;
&lt;span class="na"&gt;http.webpage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# We will use Pushbullet to send a link to all the connected devices&lt;/span&gt;
&lt;span class="c1"&gt;# with the URL of the newly saved link, but you can use any other&lt;/span&gt;
&lt;span class="c1"&gt;# services for delivering notifications and/or messages - such as&lt;/span&gt;
&lt;span class="c1"&gt;# Gotify, Twilio, Telegram or any email integration&lt;/span&gt;
&lt;span class="na"&gt;backend.pushbullet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-token&lt;/span&gt;
  &lt;span class="na"&gt;device&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-client&lt;/span&gt;

&lt;span class="na"&gt;pushbullet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build an environment from this configuration file:&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="nv"&gt;$ &lt;/span&gt;platyvenv build &lt;span class="nt"&gt;-c&lt;/span&gt; config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure that at the end of the process you have the &lt;code&gt;node&lt;/code&gt; and &lt;code&gt;npm&lt;/code&gt; executables installed - the &lt;code&gt;http.webpage&lt;/code&gt; integration uses the &lt;a href="https://github.com/postlight/mercury-parser" rel="noopener noreferrer"&gt;Mercury Parser&lt;/a&gt; API to convert web pages to Markdown.&lt;/p&gt;

&lt;p&gt;Then copy the previously created &lt;code&gt;scripts&lt;/code&gt; folder under &lt;code&gt;&amp;lt;environment-base-dir&amp;gt;/etc/platypush/scripts&lt;/code&gt;. We now want to add a new script (let's name it e.g. &lt;code&gt;webpage.py&lt;/code&gt;) that is in charge of subscribing to new messages on &lt;code&gt;notebook/save&lt;/code&gt; and use the &lt;code&gt;http.webpage&lt;/code&gt; integration to save its content in Markdown format in the repository folder. Once the parsed file is in the right directory, the previously created automation will take care of synchronizing it to the git repo.&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;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.event.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.message.event.mqtt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MQTTMessageEvent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.procedure&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;procedure&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notebook&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;repo_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/path/to/your/notebook/repo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="c1"&gt;# Base URL for your Madness Markdown instance
&lt;/span&gt;&lt;span class="n"&gt;markdown_base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://my-host/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;


&lt;span class="nd"&gt;@hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MQTTMessageEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notebook/save&lt;/span&gt;&lt;span class="sh"&gt;'&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;on_notebook_url_save_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Subscribe to new messages on the notebook/save topic.
    Such messages can contain either a URL to parse, or a
    note to create - with specified content and title.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;save_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;url&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="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@procedure&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Please specify either a URL or some Markdown content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

    &lt;span class="c1"&gt;# Create a temporary file for the Markdown content
&lt;/span&gt;    &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delete&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;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Parsing URL &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Parse the webpage to Markdown to the temporary file
&lt;/span&gt;        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http.webpage.simplify&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Sanitize title and filename
&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Note created at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&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;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;'&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Download the Markdown file to the repo
&lt;/span&gt;    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[^a-zA-Z0-9 \-_+,.]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;outfile&lt;/span&gt; &lt;span class="o"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&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="nf"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outfile&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="nf"&gt;chmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mo"&gt;0o660&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;URL &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; successfully downloaded to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Send the URL
&lt;/span&gt;    &lt;span class="n"&gt;link_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;markdown_base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pushbullet.send_note&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;link_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have a service that can listen for messages delivered on &lt;code&gt;notebook/save&lt;/code&gt;. If the message contains some Markdown content, it will directly save it to the notebook. If it contains a URL, it will use the &lt;code&gt;http.webpage&lt;/code&gt; integration to parse the web page and save it to the notebook. What we need now is a way to easily send messages to this channel while we are browsing the web. A common use-case is the one where you are reading an article on your browser (either on a computer or a mobile device) and you want to save it to your notebook to read it later through a mechanism similar to the familiar &lt;em&gt;Share&lt;/em&gt; button. Let's break down this use-case in two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The desktop (or laptop) case&lt;/li&gt;
&lt;li&gt;The mobile case&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sharing links from the desktop
&lt;/h3&gt;

&lt;p&gt;If you are reading an article on your personal computer and you want to save it to your notebook (for example to read it later on your mobile) then you can use the &lt;a href="https://git.platypush.tech/platypush/platypush-webext" rel="noopener noreferrer"&gt;Platypush browser extension&lt;/a&gt; to create a simple action that sends your current tab to the &lt;code&gt;notebook/save&lt;/code&gt; MQTT channel.&lt;/p&gt;

&lt;p&gt;Download the extension on your browser (&lt;a href="https://addons.mozilla.org/en-US/firefox/addon/platypush/" rel="noopener noreferrer"&gt;Firefox version&lt;/a&gt;, &lt;a href="https://chrome.google.com/webstore/detail/platypush/aphldjclndofhflbbdnmpejbjgomkbie" rel="noopener noreferrer"&gt;Chrome version&lt;/a&gt;) - more information about the Platypush browser extension is available in a &lt;a href="https://blog.platypush.tech/article/One-browser-extension-to-rule-them-all" rel="noopener noreferrer"&gt;previous article&lt;/a&gt;. Then, click on the extension icon in the browser and add a new connection to a Platypush host - it could either be your own machine or any of the notebook clients you have configured.&lt;/p&gt;

&lt;p&gt;Side note: the extension only works if the target Platypush machine has &lt;code&gt;backend.http&lt;/code&gt; (i.e. the web server) enabled, as it is used to dispatch messages over the Platypush API. This wasn't required by the previous set up, but you can now select one of the devices to expose a web server by simply adding a &lt;code&gt;backend.http&lt;/code&gt; section to the configuration file and setting &lt;code&gt;enabled: True&lt;/code&gt; (by default the web server will listen on the port 8008).&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%2Fblog.platypush.tech%2Fimg%2Fextension-2.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%2Fblog.platypush.tech%2Fimg%2Fextension-2.png" alt="Platypush web extension first screen"&gt;&lt;/a&gt;&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%2Fblog.platypush.tech%2Fimg%2Fextension-3.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%2Fblog.platypush.tech%2Fimg%2Fextension-3.png" alt="Platypush web extension second screen"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then from the extension configuration panel select your host -&amp;gt; Run Action. Wait for the autocomplete bar to populate (it may take a while the first time, since it has to inspect all the methods in all the enabled packages) and then create a new &lt;code&gt;mqtt.publish&lt;/code&gt; action that sends a message with the current URL over the &lt;code&gt;notebook/save&lt;/code&gt; channel:&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%2Fblog.platypush.tech%2Fimg%2Fself-hosted-notebook-extension-1.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%2Fblog.platypush.tech%2Fimg%2Fself-hosted-notebook-extension-1.png" alt="URL save extension action"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;em&gt;Save Action&lt;/em&gt; button at the bottom of the page, give your action a name and, optionally, an icon, a color and a set of tags. You can also select a keybinding between Ctrl+Alt+0 and Ctrl+Alt+9 to automatically run your action without having to grab the mouse.&lt;/p&gt;

&lt;p&gt;Now browse to any web page that you want to save, run the action (either by clicking on the extension icon and selecting it or through the keyboard shortcut) and wait a couple of seconds. You should soon receive a Pushbullet notification with a link to the parsed content and the repo should get updated as well on all of your devices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing links from mobile devices
&lt;/h3&gt;

&lt;p&gt;An easy way to share links to your notebook through an Android device is to leverage &lt;a href="https://tasker.joaoapps.com/" rel="noopener noreferrer"&gt;Tasker&lt;/a&gt; with the &lt;a href="https://joaoapps.com/autoshare/what-it-is/" rel="noopener noreferrer"&gt;AutoShare&lt;/a&gt; plugin, and choose an app like &lt;a href="https://play.google.com/store/apps/details?id=in.dc297.mqttclpro" rel="noopener noreferrer"&gt;MQTT Client&lt;/a&gt; that comes with a Tasker integration. You may then create a new AutoShare intent named e.g. &lt;em&gt;Save URL&lt;/em&gt;, create a Tasker task associated to it that uses the MQTT Client integration to send the message with the URL to the right MQTT topic. When you are browsing a web page that you'd like to save then you simply click on the &lt;em&gt;Share&lt;/em&gt; button and select &lt;em&gt;AutoShare Command&lt;/em&gt; in the popup window, then select the action you have created.&lt;/p&gt;

&lt;p&gt;However, even though I really appreciate the features provided by Tasker, its ecosystem and the developer behind it (I have been using it for more than 10 years), I am on a path of moving more and more of my automation away from it. Firstly, because it's a paid app with paid services, and the whole point of setting up this whole automation is to have the same quality of a paid service without having to pay for - we host it, we own it. Secondly, it's not an open-source app, and it's notably tricky to migrate configurations across devices.&lt;/p&gt;

&lt;p&gt;Termux also provides a mechanism for &lt;a href="https://wiki.termux.com/wiki/Intents_and_Hooks" rel="noopener noreferrer"&gt;intents and hooks&lt;/a&gt;, and we can easily create a sharing intent for the notebook by creating a script under &lt;code&gt;~/bin/termux-url-opener&lt;/code&gt;. Make sure that the binary file is executable and that you have &lt;code&gt;Termux:GUI&lt;/code&gt; installed for support for visual widgets:&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="c"&gt;#!/data/data/com.termux/files/usr/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;arg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# termux-dialog-radio show a list of mutually exclusive options and returns&lt;/span&gt;
&lt;span class="c"&gt;# the selection in JSON format. The options need to be provided over the -v&lt;/span&gt;
&lt;span class="c"&gt;# argument and they are comma-separated&lt;/span&gt;
&lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;termux-dialog radio &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s1"&gt;'Select an option'&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'Save URL,some,other,options'&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.text'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="s1"&gt;'Save URL'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | python
from platypush.utils import run

run('mqtt.publish', topic='notebook/save', msg={'url': '&lt;/span&gt;&lt;span class="nv"&gt;$arg&lt;/span&gt;&lt;span class="sh"&gt;'})
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;      &lt;span class="p"&gt;;;&lt;/span&gt;

    &lt;span class="c"&gt;# You can add some other actions here&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now browse to a page that you want to save from your mobile device, tap the &lt;em&gt;Share&lt;/em&gt; button, select &lt;em&gt;Termux&lt;/em&gt; and select the &lt;em&gt;Save URL&lt;/em&gt; option. Everything should work out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivering RSS digests to your notebook
&lt;/h2&gt;

&lt;p&gt;As a last step in our automation set up, let's consider the use-case where you want a digest of the new content from your favourite source (your favourite newspaper, magazine, blog etc.) to be automatically delivered on a periodic basis to your notebook in readable format.&lt;/p&gt;

&lt;p&gt;It's relatively easy to set up such automation with the building blocks we have put in place and the Platypush &lt;a href="https://docs.platypush.tech/platypush/plugins/rss.html" rel="noopener noreferrer"&gt;&lt;code&gt;rss&lt;/code&gt;&lt;/a&gt; integration. Add an &lt;code&gt;rss&lt;/code&gt; section to the configuration file of any of your clients with the &lt;code&gt;http.webpage&lt;/code&gt; integration. It will contain the RSS sources you want to subscribe to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;rss&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;subscriptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://source1.com/feed/rss&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://source2.com/feed/rss&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://source3.com/feed/rss&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then either rebuild the virtual environment (&lt;code&gt;platyvenv build -c config.yaml&lt;/code&gt;) or manually install the required dependency in the existing environment (&lt;code&gt;pip install feedparser&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The RSS integration will trigger a &lt;a href="https://docs.platypush.tech/platypush/events/rss.html#platypush.message.event.rss.NewFeedEntryEvent" rel="noopener noreferrer"&gt;&lt;code&gt;NewFeedEntryEvent&lt;/code&gt;&lt;/a&gt; whenever an entry is added to an RSS feed you are subscribed to. We now want to create a logic that reacts to such events and does the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Whenever a new entry is created on a subscribed feed, add the corresponding URL to a queue of links to process&lt;/li&gt;
&lt;li&gt;A cronjob that runs on a specified basis will collect all the links in the queue, parse the content of the webpages and save them in a &lt;code&gt;digests&lt;/code&gt; folder on the notebook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create a new script under &lt;code&gt;$PREFIX/etc/platypush/scripts&lt;/code&gt; named e.g. &lt;code&gt;digests.py&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RLock&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.cron&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cron&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.event.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.message.event.rss&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NewFeedEntryEvent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;platypush.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.notebook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;digest-generator&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Path to a text file where you'll store the processing queue
# for the feed entries - one URL per line
&lt;/span&gt;&lt;span class="n"&gt;queue_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/path/to/feeds/processing/queue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="c1"&gt;# Lock to ensure consistency when writing to the queue
&lt;/span&gt;&lt;span class="n"&gt;queue_path_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# The digests path will be a subfolder of the repo_path
&lt;/span&gt;&lt;span class="n"&gt;digests_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/digests&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;


&lt;span class="nd"&gt;@hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewFeedEntryEvent&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;on_new_feed_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Subscribe to new RSS feed entry events and add the
    corresponding URLs to a processing queue.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;queue_path_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;'&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0 4 * * *&lt;/span&gt;&lt;span class="sh"&gt;'&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;digest_generation_cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    This cronjob runs every day at 4AM local time.
    It processes all the URLs in the queue, it generates a digest
    with the parsed content and it saves it in the notebook folder.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Running digest generation cronjob&lt;/span&gt;&lt;span class="sh"&gt;'&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;queue_path_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;md_files&lt;/span&gt; &lt;span class="o"&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;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="c1"&gt;# Create a temporary file for the Markdown content
&lt;/span&gt;                    &lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delete&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;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Parsing URL &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="c1"&gt;# Parse the webpage to Markdown to the temporary file
&lt;/span&gt;                    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http.webpage.simplify&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;md_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&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;md_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;No URLs to process&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digests_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&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;exist_ok&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;digest_file&lt;/span&gt; &lt;span class="o"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digests_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_digest&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;digest_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;# Digest generated on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;md_file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;md_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;digest_content&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;'&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Clean up the queue
&lt;/span&gt;            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;finally&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;md_file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;md_files&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="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now restart the Platypush service. On the first start after configuring the &lt;code&gt;rss&lt;/code&gt; integration it should trigger a bunch of &lt;code&gt;NewFeedEntryEvent&lt;/code&gt; with all the newly seen content from the subscribed feed. Once the cronjob runs, it will process all these pending requests and it will generate a new digest in your notebook folder. Since we previously set up an automation to monitor changes in this folder, the newly created file will trigger a git sync as well as broadcast sync request on MQTT. At there you go - your daily or weekly subscriptions, directly delivered to your custom notebook!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;In this article we have learned:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How to design a distributed architecture to synchronize content across multiple devices using Platypush scripts as the glue between a git repository and an MQTT broker.&lt;/li&gt;
&lt;li&gt;How to manage a notebook based on Markdown and which popular options are available for the visualization - Github/Gitlab, Obsidian, NextCloud Notes, Madness.&lt;/li&gt;
&lt;li&gt;How to install a Platypush virtual environment on the fly from a configuration file through &lt;code&gt;platyvenv&lt;/code&gt; command (in the previous articles I mainly targeted manual installations). Just for you to know, a &lt;code&gt;platydock&lt;/code&gt; command is also available to create Docker containers on the fly from a configuration file, but given the hardware requirements or specific dependency chains that some integrations may require the mileage of &lt;code&gt;platydock&lt;/code&gt; may vary.&lt;/li&gt;
&lt;li&gt;How to install and run Platypush directly on Android through Termux. This is actually quite huge: in this specific article we targeted a use-case for folder synchronization between mobile and desktop, but given the high number of integrations provided by Platypush, as well as the powerful scripts provided by &lt;code&gt;Termux:API&lt;/code&gt;, it's relatively easy to use Platypush to set up automations that replace the need of paid (and closed-source) services like Tasker.&lt;/li&gt;
&lt;li&gt;How to use the &lt;code&gt;http.webpage&lt;/code&gt; integration to distill web pages into readable Markdown.&lt;/li&gt;
&lt;li&gt;How to push links to our automation chain through a desktop browser (using the Platypush browser extension) or mobile (using the &lt;code&gt;termux-url-opener&lt;/code&gt; mechanism).&lt;/li&gt;
&lt;li&gt;How to use the &lt;code&gt;rss&lt;/code&gt; integration to subscribe to feeds, and how to hook it to &lt;code&gt;http.webpage&lt;/code&gt; and cronjobs to generate periodic digests delivered to our notebook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You should now have some solid tools to build your own automated notebook. A few ideas on possible follow-ups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use your notebook to manage databases (a feature provided by Notion) in CSV format.&lt;/li&gt;
&lt;li&gt;Set up a similar distributed sync mechanism to synchronize photos across devices.&lt;/li&gt;
&lt;li&gt;Host your own Markdown-based wiki or website built on top of such an automation pipeline, so on each update the website is automatically refreshed with the new content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Happy hacking!&lt;/p&gt;

</description>
      <category>platypush</category>
      <category>python</category>
      <category>git</category>
      <category>mqtt</category>
    </item>
    <item>
      <title>Set up self-hosted CI/CD git pipelines with Platypush</title>
      <dc:creator>Fabio Manganiello</dc:creator>
      <pubDate>Sun, 07 Mar 2021 21:11:52 +0000</pubDate>
      <link>https://forem.com/blacklight/set-up-self-hosted-ci-cd-git-pipelines-with-platypush-1818</link>
      <guid>https://forem.com/blacklight/set-up-self-hosted-ci-cd-git-pipelines-with-platypush-1818</guid>
      <description>&lt;p&gt;Git automation, either in the form of &lt;a href="https://docs.gitlab.com/ee/ci/pipelines/"&gt;Gitlab pipelines&lt;/a&gt; or &lt;a href="https://github.com/features/actions"&gt;Github actions&lt;/a&gt;, is amazing. It enables you to automate a lot of software maintenance tasks (testing, monitoring, mirroring repositories, generating documentation, building and distributing packages etc.) that until a couple of years ago used to take a lot of development time. These forms of automation have democratized CI/CD, bringing to the open-source world benefits that until recently either belonged mostly to the enterprise world (such as TeamCity) or had a steep curve in terms of configuration (such as Jenkins).&lt;/p&gt;

&lt;p&gt;I have been using Github actions myself for a long time on the Platypush codebase, with a Travis-CI integration to run integration tests online and a ReadTheDocs integration to automatically generate online documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  You and whose code?
&lt;/h2&gt;

&lt;p&gt;However, a few things have changed lately, and I don't feel like I should rely much on the tools mentioned above for my&lt;br&gt;
CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;Github has &lt;a href="https://www.bleepingcomputer.com/news/software/github-threatens-to-ban-users-who-bypass-youtube-dl-takedown/"&gt;too often taken the wrong side&lt;/a&gt; in DMCA disputes since it's been acquired by Microsoft. The &lt;a href="https://devrant.com/rants/3366288/seems-like-even-githubs-ceo-has-now-chimed-in-on-the-youtube-dl-takedown-https-t"&gt;CEO of Github has in the meantime tried to redeem himself&lt;/a&gt;, but the damage in the eyes of many developers, myself included, was done, all in spite of the friendly olive branch to the community handed over IRC. Most of all, that doesn't change the fact that Github has &lt;a href="https://github.com/github/dmca/tree/master/2020"&gt;taken down more than 2000 other repos&lt;/a&gt; in 2020 alone, often without any appeal or legal support - the CEO bowed down in the case of &lt;code&gt;youtube-dl&lt;/code&gt; only because of the massive publicity that the takedown attracted. Moreover, Github has yet to overcome its biggest contradiction: it advertises itself like the home for open-source software, but its own source code is not open-source, so you can't spin up your own instance on your own server. There's also increasing evidence in support of my initial suspicion that the Github acquisition was nothing but another old-school &lt;a href="https://en.wikipedia.org/wiki/Embrace,_extend,_and_extinguish"&gt;Microsoft triple-E operation&lt;/a&gt;. When you want to clone a Github repo you won't be prompted anymore with the HTTPS/SSH link by default, you'll be prompted with the Github CLI command, which extends the standard &lt;code&gt;git&lt;/code&gt; command, but it introduces a couple of naming inconsistencies here and there. They could have contributed to improve the &lt;code&gt;git&lt;/code&gt; tool for everyone's benefit instead of providing their new tool as the new default, but they have opted not to do so. I'm old enough to have seen quite a few of these examples in the past, and it never ended well for the &lt;em&gt;extended&lt;/em&gt; party. As a consequence of these actions, I have &lt;a href="https://git.platypush.tech/platypush"&gt;moved the Platypush repos to a self-hosted Gitlab instance&lt;/a&gt; - which comes with much more freedom, but also no more Github actions.&lt;/p&gt;

&lt;p&gt;And, after the announcement of the &lt;a href="https://devclass.com/2020/11/25/travis-ci-open-source-engagement/"&gt;planned migration from travis-ci.org to travis-ci.com&lt;/a&gt;, with greater focus on enterprise, a limited credit system for open-source projects and a migration process that is largely manual, I have also realized that Travis-CI is another service that can't be relied upon anymore when it comes to open-source software. And, again, Travis-CI is plagued by the same contradiction as Github - it claims to be open-source friendly, but it's not open-source itself, and you can't install it on your own machine.&lt;/p&gt;

&lt;p&gt;ReadTheDocs, luckily, seems to be still coherent with its mission of supporting open-source developers, but I'm also keeping an eye on them just in case :) &lt;/p&gt;
&lt;h2&gt;
  
  
  Building a self-hosted CI/CD pipeline
&lt;/h2&gt;

&lt;p&gt;Even though abandoning closed-source and unreliable cloud development tools is probably the right thing to do, that leaves a hole behind: how do we bring the simplicity of the automation provided by those tools to our new home - and, preferably, in such a format that it can be hosted and moved anywhere?&lt;/p&gt;

&lt;p&gt;Github and Travis-CI provide a very easy way of setting up CI/CD pipelines. You read the documentation, upload a YAML file to your repo, and all the magic happens. I wanted to build something that was that easy to configure, but that could run anywhere, not only in someone else's cloud.&lt;/p&gt;

&lt;p&gt;Building a self-hosted pipeline, however, also brings its advantages. Besides liberating yourself of the concern of handing your hard-worked code to someone else who can either change their mind about their mission, or take it down overnight, you have the freedom of setting up the environment for build and test however you please and customize it however you please. And you can easily set up integrations such as automated notifications over whichever channel you like, without the headache of installing and configuring all the dependencies to run on someone else's cloud.&lt;/p&gt;

&lt;p&gt;In this article we'll how to use Platypush to set up a pipeline that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Reacts to push and tag events on a Gitlab or Github repository and runs custom Platypush actions in YAML format or Python code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automatically mirrors the new commits and tags to another repo - in my case, from Gitlab to Github.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Runs a suite of tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the tests succeed, then it proceeds with packaging the new version of the codebase - in my case, I run the automation to automatically create the new &lt;a href="https://aur.archlinux.org/packages/platypush-git"&gt;&lt;code&gt;platypush-git&lt;/code&gt;&lt;/a&gt; package for Arch Linux on new pushes, and the new &lt;a href="https://aur.archlinux.org/packages/platypush"&gt;&lt;code&gt;platypush&lt;/code&gt;&lt;/a&gt; Arch package and the &lt;a href="https://pypi.org/project/platypush/"&gt;&lt;code&gt;pip&lt;/code&gt; package&lt;/a&gt; on new tags.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the tests fail, it sends a notification (over &lt;a href="https://docs.platypush.tech/en/latest/platypush/plugins/mail.smtp.html"&gt;email&lt;/a&gt;, &lt;a href="https://docs.platypush.tech/en/latest/platypush/plugins/chat.telegram.html"&gt;Telegram&lt;/a&gt;, &lt;a href="https://docs.platypush.tech/en/latest/platypush/plugins/pushbullet.html"&gt;Pushbullet&lt;/a&gt; or &lt;a href="https://docs.platypush.tech/en/latest/"&gt;whichever plugin supported by Platypush&lt;/a&gt;). Also send a notification if the latest run of tests has succeeded and the previous one was failing.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: since I have moved my projects to a self-hosted Gitlab server, I could have also relied on the native Gitlab CI/CD pipelines, but I have eventually opted not to do so for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Setting up the whole Docker+Kubernetes automation required for the CI/CD pipeline proved to be a quite cumbersome process. Additionally, it may require some properly beefed machine in order to run smoothly, while ideally I wanted something that could run even on a RaspberryPi, provided that the building and testing processes aren't too resource-heavy themselves.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The alternative provided by Gitlab to setting up your Kubernetes instance and configuring the Gitlab integration is to get a bucket on the cloud to spin a container that runs all you have to run. But if I have gone so far to set up my own self-hosted infrastructure for hosting my code, I certainly don't want to give up on the last mile in exchange of a small discount on the Google Cloud services :)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, if either you have enough hardware resources and time to set up your own Kubernetes infrastructure to integrate with Gitlab, or you don't mind running your CI/CD logic on the Google cloud, Gitlab CI/CD pipelines are something that you may consider - if you don't have the constraints above then they are very powerful, flexible and easy to set up.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installing Platypush
&lt;/h2&gt;

&lt;p&gt;Let's start by installing Platypush with the required integrations.&lt;/p&gt;

&lt;p&gt;If you want to set up an automation that reacts on Gitlab events then you'll only need the &lt;code&gt;http&lt;/code&gt; integration, since we'll use &lt;a href="https://docs.gitlab.com/ee/user/project/integrations/webhooks.html"&gt;Gitlab webhooks&lt;/a&gt; to trigger the automation:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'platypush[http]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to set up the automation on a Github repo you'll only have one or two additional dependencies, installed through the &lt;code&gt;github&lt;/code&gt; integration:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'platypush[http,github]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to be notified of the status of your builds then you may want to install the integration required by the communication mean that you want to use. We'll use &lt;a href="https://pushbullet.com"&gt;Pushbullet&lt;/a&gt; in this example because it's easy to set up and it natively supports notifications both on mobile and desktop:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'platypush[pushbullet]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feel free however to pick anything else - for instance, you can refer to &lt;a href="https://blog.platypush.tech/article/Build-a-bot-to-communicate-with-your-smart-home-over-Telegram"&gt;this article&lt;/a&gt; for a Telegram set up or &lt;a href="https://blog.platypush.tech/article/Deliver-customized-newsletters-from-RSS-feeds-with-Platypush"&gt;this article&lt;/a&gt; for a mail set up, or take a look at the &lt;a href="https://docs.platypush.tech/en/latest/platypush/plugins/twilio.html"&gt;Twilio integration&lt;/a&gt; if you want automated notifications over SMS or Whatsapp.&lt;/p&gt;

&lt;p&gt;Once installed, create a &lt;code&gt;~/.config/platypush/config.yaml&lt;/code&gt; file that contains the service configuration - for now we'll just enable the web server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The backend listens on port 8008 by default&lt;/span&gt;
&lt;span class="na"&gt;backend.http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting up a Gitlab hook
&lt;/h2&gt;

&lt;p&gt;Gitlab webhooks are a very simple and powerful way of triggering things when something happens on a Gitlab repo. All you have to do is setting up a URL that should be called upon a repository event (push, tag, new issue, merge request etc.), and set up an automation on the endpoint that reacts to the event.&lt;/p&gt;

&lt;p&gt;The only requirement for this mechanism to work is that the endpoint must be reachable from the Gitlab host - it means that the host running the Platypush web service must either be publicly accessible, on the same network or VPN as the Gitlab host, or the Platypush web port must be tunneled/proxied to the Gitlab host.&lt;/p&gt;

&lt;p&gt;Platypush offers a very easy way to expose custom endpoints through the &lt;a href="https://docs.platypush.tech/en/latest/platypush/events/http.hook.html"&gt;&lt;code&gt;WebhookEvent&lt;/code&gt;&lt;/a&gt;. All you have to do is set up an event hook that reacts to a &lt;code&gt;WebhookEvent&lt;/code&gt; at a specific endpoint. For example, create a new event hook under &lt;code&gt;~/.config/platypush/scripts/gitlab.py&lt;/code&gt;:&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;platypush.event.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.message.event.http.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebhookEvent&lt;/span&gt;

&lt;span class="c1"&gt;# Token to be used to authenticate the calls from Gitlab
&lt;/span&gt;&lt;span class="n"&gt;gitlab_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'YOUR_TOKEN_HERE'&lt;/span&gt;


&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebhookEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'repo-push'&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;on_repo_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Check that the token provided over the
&lt;/span&gt;    &lt;span class="c1"&gt;# X-Gitlab-Token header is valid
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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;'X-Gitlab-Token'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;gitlab_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; \
      &lt;span class="s"&gt;'Invalid Gitlab token'&lt;/span&gt;

    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Add your logic here'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hook will react when an HTTP request is received on &lt;code&gt;http://your-host:8008/hook/repo-push&lt;/code&gt;. Note that, unlike most of the other Platypush endpoints, custom hooks are &lt;em&gt;not&lt;/em&gt; authenticated - that's because they may be called from any context, and you don't necessarily want to share your Platypush instance credentials or token with 3rd-parties.  Instead, it's up to you to implement whichever authentication policy you like over the requests.&lt;/p&gt;

&lt;p&gt;After adding your endpoint, start Platypush:&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="nv"&gt;$ &lt;/span&gt;platypush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, in order to set up a new webhook, navigate to your Gitlab project -&amp;gt; Settings -&amp;gt; Webhooks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dS7-LI8h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.platypush.tech/img/gitlab-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dS7-LI8h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.platypush.tech/img/gitlab-1.png" alt="Gitlab webhook setup" width="880" height="809"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter the URL to your webhook and the secret token and select the events you want to react to - in this example, we'll select new push events.&lt;/p&gt;

&lt;p&gt;You can now test the endpoint through the Gitlab interface itself. If it all went well, you should see a &lt;code&gt;Received event&lt;/code&gt; line with a content like this on the standard output or log file of Platypush:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platypush-host"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gitlab-host"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platypush.message.event.http.hook.WebhookEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hook"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo-push"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"object_kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"push"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"event_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"push"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"before"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"previous-commit-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"after"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"current-commit-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refs/heads/master"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"checkout_sha"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"current-commit-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"user_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your User"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"user_username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"youruser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"user_email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you@email.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"user_avatar"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"path to your avatar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"project_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Project description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"web_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://git.platypush.tech/platypush/platypush"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"avatar_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://git.platypush.tech/uploads/-/system/project/avatar/3/icon-256.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"git_ssh_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git@git.platypush.tech:platypush/platypush.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"git_http_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://git.platypush.tech/platypush/platypush.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"visibility_level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"path_with_namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platypush/platypush"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"default_branch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"master"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ci_config_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"homepage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://git.platypush.tech/platypush/platypush"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git@git.platypush.tech:platypush/platypush.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ssh_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git@git.platypush.tech:platypush/platypush.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"http_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://git.platypush.tech/platypush/platypush.git"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"commits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"current-commit-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This is a commit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This is a commit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2021-03-06T20:02:25+01:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://git.platypush.tech/platypush/platypush/-/commit/current-commit-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your Name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you@email.com"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"added"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"modified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"tests/my_test.py"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"removed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"total_commits_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"push_options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git@git.platypush.tech:platypush/platypush.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Project description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"homepage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://git.platypush.tech/platypush/platypush"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"git_http_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://git.platypush.tech/platypush/platypush.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"git_ssh_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git@git.platypush.tech:platypush/platypush.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"visibility_level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"User-Agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GitLab/version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"X-Gitlab-Event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Push Hook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"X-Gitlab-Token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR GITLAB TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Connection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"close"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platypush-host:8008"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Content-Length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lenght"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are all fields provided on the event object that you can use in your hook to build your custom logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up a Github integration
&lt;/h2&gt;

&lt;p&gt;If you want to keep using Github but run the CI/CD pipelines on another host with no dependencies on the Github actions, you can leverage the &lt;a href="https://docs.platypush.tech/en/latest/platypush/backend/github.html"&gt;Github backend&lt;/a&gt; to monitor your repos and fire &lt;a href="https://docs.platypush.tech/en/latest/platypush/events/github.html"&gt;Github events&lt;/a&gt; that you can build your hooks on when something happens.&lt;/p&gt;

&lt;p&gt;First, head to your Github profile to create a new &lt;a href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token"&gt;API access token&lt;/a&gt;. Then add the configuration to &lt;code&gt;~/.config/platypush/config.yaml&lt;/code&gt; under the &lt;code&gt;backend.github&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;backend.github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your_user&lt;/span&gt;
  &lt;span class="na"&gt;user_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your_token&lt;/span&gt;
  &lt;span class="c1"&gt;# Optional list of repos to monitor (default: all user repos)&lt;/span&gt;
  &lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://github.com/you/myrepo1.git&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://github.com/you/myrepo2.git&lt;/span&gt;

  &lt;span class="c1"&gt;# How often the backend should poll for updates (default: 60 seconds)&lt;/span&gt;
  &lt;span class="na"&gt;poll_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;

  &lt;span class="c1"&gt;# Maximum events that will be triggered if a high number of events has&lt;/span&gt;
  &lt;span class="c1"&gt;# been triggered since the last poll (default: 10)&lt;/span&gt;
  &lt;span class="na"&gt;max_events_per_scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the service, and on e.g. the first repository push event you should see a &lt;code&gt;Received event&lt;/code&gt; log line like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-host"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-host"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platypush.message.event.github.GithubPushEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"actor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"login"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"display_login"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.github.com/users/you"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"avatar_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://avatars.githubusercontent.com/u/1234?"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PushEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you/myrepo1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.github.com/repos/you/myrepo1"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2021-03-03T18:20:27+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"push_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"distinct_size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refs/heads/master"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"head"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"current-commit-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"before"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"previous-commit-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"commits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"sha"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"current-commit-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you@email.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This is a commit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"distinct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.github.com/repos/you/myrepo1/commits/current-commit-id"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can easily create an event hook that reacts to such events to run your automation - e.g. under &lt;code&gt;~/.config/platypush/scripts/github.py&lt;/code&gt;:&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;platypush.event.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.message.event.github&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GithubPushEvent&lt;/span&gt;


&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GithubPushEvent&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;on_repo_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="c1"&gt;# Run this action only for a specific repo
&lt;/span&gt;  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;'you/myrepo1'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;

  &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Add your logic here'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here you go - you should now be ready to create your automation routines on Github events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated repository mirroring
&lt;/h2&gt;

&lt;p&gt;Even though I have moved the Platypush repos to a self-hosted domain, I still keep a mirror of them on Github. That's because lots of people have already cloned the repos over the years and may lose updates if they haven't seen the announcement about the transfer. Also, registering to a new domain is often a barrier for users who want to create issues. So, even though me and Github are no longer friends, I still need a way to easily mirror each new commit on my domain to Github - but you might as well have another compelling case for backing up/mirroring your repos. The way I'm currently achieving this is by cloning the main instance of the repo on the machine that runs the Platypush service:&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="nv"&gt;$ &lt;/span&gt;git clone git@git.you.com:you/myrepo.git /opt/repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a new remote that points to your mirror repo:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/repo
&lt;span class="nv"&gt;$ &lt;/span&gt;git remote add mirror git@github.com:/you/myrepo.git
&lt;span class="nv"&gt;$ &lt;/span&gt;git fetch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then try a first &lt;code&gt;git push --mirror&lt;/code&gt; to make sure that the repos are aligned and all conflicts are solved:&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="nv"&gt;$ &lt;/span&gt;git push &lt;span class="nt"&gt;--mirror&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; mirror
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a new &lt;code&gt;sync_to_mirror&lt;/code&gt; function in your Platypush script file that 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;import&lt;/span&gt; &lt;span class="nn"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;

&lt;span class="n"&gt;repo_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'/opt/repo'&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;sync_to_mirror&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Synchronizing commits to mirror'&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;chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Pull the updates from the main repo
&lt;/span&gt;    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;'git'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'pull'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'--rebase'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'origin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'master'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="c1"&gt;# Sync the updates to the repo
&lt;/span&gt;    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;'git'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'push'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'--mirror'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'-v'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'mirror'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Synchronizing commits to mirror: DONE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And just call it from the previously defined &lt;code&gt;on_repo_push&lt;/code&gt; hook, either the Gitlab or Github variant:&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;# ...
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_repo_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;    &lt;span class="n"&gt;sync_to_mirror&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now on each push the repository clone stored under &lt;code&gt;/opt/repo&lt;/code&gt; will be updated and any new commits and tags will be mirrored to the mirror repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running tests
&lt;/h2&gt;

&lt;p&gt;If our project is properly set up, then it probably has a suite of unit/integration tests that is supposed to be run on each change to verify that nothing is broken. It's quite easy to configure the previously created hook so that it runs the tests on each push. For instance, if your tests are stored under the &lt;code&gt;tests&lt;/code&gt; folder of your project and you use &lt;code&gt;pytest&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="nn"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pathlib&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;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.event.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.message.event.http.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebhookEvent&lt;/span&gt;

&lt;span class="c1"&gt;# Path where the latest version of the repo will be cloned
&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'/tmp/repo'&lt;/span&gt;
&lt;span class="c1"&gt;# Path where the results of the tests will be stored
&lt;/span&gt;&lt;span class="n"&gt;logs_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'/var/log/tests'&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;run_tests&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
  &lt;span class="c1"&gt;# Clone the repo in /tmp
&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;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignore_errors&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;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;'git'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'clone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'git@git.you.com:you/myrepo.git'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tmp_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;chdir&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'tests'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Run the tests
&lt;/span&gt;    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;'pytest'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                             &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PIPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PIPE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;stdout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;communicate&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&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="n"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="c1"&gt;# Write the stdout to a logfile
&lt;/span&gt;    &lt;span class="n"&gt;pathlib&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;logs_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&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;exist_ok&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;logfile&lt;/span&gt; &lt;span class="o"&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logs_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_'&lt;/span&gt;
                           &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"PASSED"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s"&gt;"FAILED"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.log'&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;logfile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'w'&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;finally&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;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignore_errors&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="c1"&gt;# Return True if the tests passed, False otherwise
&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;

&lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebhookEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'repo-push'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# or
# @hook(GithubPushEvent)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_repo_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="c1"&gt;# ...
&lt;/span&gt;  &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon push event, the latest version of the repo will be cloned under &lt;code&gt;/tmp/repo&lt;/code&gt; and the suite of tests will be run.  The output of each session will be stored under &lt;code&gt;/var/log/tests&lt;/code&gt; in a file formatted like &lt;code&gt;&amp;lt;ISO timestamp&amp;gt;_&amp;lt;PASSED|FAILED&amp;gt;.log&lt;/code&gt;. To make things even more robust, you can create a new virtual environment under the temporary directory, install your repo with all of its dependency in the new virtual environment and run the tests from there, or spin a Docker instance with the required configuration, to make sure that the tests would also pass on a fresh installation and prevent the &lt;em&gt;"but if works on my box"&lt;/em&gt; issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serve the test results over HTTP
&lt;/h2&gt;

&lt;p&gt;Now you can simply serve &lt;code&gt;/var/log/tests&lt;/code&gt; over an HTTP server and the logs can be accessed from your browser. Simple case:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /var/log/tests
&lt;span class="nv"&gt;$ &lt;/span&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logs will be served on &lt;code&gt;http://host:8000&lt;/code&gt;. You can also serve the directory through a proper web server like nginx or Apache.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--R2MJUkOg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.platypush.tech/img/ci-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--R2MJUkOg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.platypush.tech/img/ci-1.png" alt="CI logs over HTTP" width="679" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It doesn't come with all the bells and whistles of the Jenkins or Travis-CI UI, but it's simple and good enough for its job - and it's not hard to extend it with a fancier UI if you like.&lt;/p&gt;

&lt;p&gt;Another nice addition is to download some of those nice &lt;em&gt;passed/failed&lt;/em&gt; badge images that you find on many Github repositories to your Platypush box. When a test run completes, just edit your hook to copy the associated banner image (e.g. &lt;code&gt;passed.svg&lt;/code&gt; or &lt;code&gt;failed.svg&lt;/code&gt;) to e.g. &lt;code&gt;/var/log/tests/status.svg&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;shutil&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;run_tests&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;    &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;badge_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'/path/to/passed.svg'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s"&gt;'/path/to/failed.svg'&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;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;badge_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;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;logs_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'status.svg'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then embed the status in your &lt;code&gt;README.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;![Tests Status&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;http://your-host:8000/status.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;](http://your-host:8000)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there you go - you can now show off a dynamically generated and self-hosted status badge on your README without relying on any cloud runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic build and test notifications
&lt;/h2&gt;

&lt;p&gt;Another useful feature of the popular cloud services is the ability to send notification when a build status changes.  This is quite easy to set up with Platypush, as the application provides several plugins for messaging. Let's look at an example where a change in the status of our tests triggers a notification to our Pushbullet account, which can be delivered both to our desktop and mobile devices. &lt;a href="https://pushbullet.com"&gt;Download the Pushbullet app&lt;/a&gt; if you want the notifications to be delivered to your mobile, get an &lt;a href="https://docs.pushbullet.com/"&gt;API token&lt;/a&gt; and then install the dependencies for the Pushbullet integration for Platypush:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'platypush[pushbullet]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure the Pushbullet plugin and backend in &lt;code&gt;~/.config/platypush/config.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;backend.pushbullet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_PUSHBULLET_TOKEN&lt;/span&gt;
    &lt;span class="na"&gt;device&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;platypush&lt;/span&gt;

&lt;span class="na"&gt;pushbullet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now simply modify your push hook to send a notification when the status of build changes. We will also use the &lt;a href="https://docs.platypush.tech/en/latest/platypush/plugins/variable.html"&gt;&lt;code&gt;variable&lt;/code&gt; plugin&lt;/a&gt; to retrieve and store the latest status, so that notifications are triggered only when the status changes:&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;platypush.context&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_plugin&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.event.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.message.event.http.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebhookEvent&lt;/span&gt;

&lt;span class="c1"&gt;# Name of the variable that holds the latest run status
&lt;/span&gt;&lt;span class="n"&gt;last_tests_passed_var&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'LAST_TESTS_PASSED'&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;run_tests&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
  &lt;span class="c1"&gt;# ...
&lt;/span&gt;  &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="c1"&gt;# ...
&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;

&lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebhookEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'repo-push'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# or
# @hook(GithubPushEvent)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_repo_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'variable'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;pushbullet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'pushbullet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Get the status of the last run
&lt;/span&gt;  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;variable&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="n"&gt;last_tests_passed_var&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;
  &lt;span class="n"&gt;last_tests_passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&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="n"&gt;last_tests_passed_var&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="c1"&gt;# ...
&lt;/span&gt;
  &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;last_tests_passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pushbullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'The tests are now PASSING'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="c1"&gt;# If device is not set then the notification will
&lt;/span&gt;                         &lt;span class="c1"&gt;# be sent to all the devices connected to the account
&lt;/span&gt;                         &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'my-mobile-name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;last_tests_passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pushbullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'The tests are now FAILING'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'my-mobile-name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Update the last_test_passed variable
&lt;/span&gt;  &lt;span class="n"&gt;variable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last_tests_passed_var&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The nice addition of this approach is that any other Platypush device with the Pushbullet backend enabled and connected to the same account will receive a &lt;a href="https://docs.platypush.tech/en/latest/platypush/events/pushbullet.html#platypush.message.event.pushbullet.PushbulletEvent"&gt;&lt;code&gt;PushbulletEvent&lt;/code&gt;&lt;/a&gt; when a Pushbullet note is sent, and you can easily leverage this to build some downstream logic with hooks that react to these events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous delivery
&lt;/h2&gt;

&lt;p&gt;Once we have a logic in place that automatically mirrors and tests our code and notifies us about status changes, we can take things a step further and set up our pipeline to also build a package for our applications if the tests are successful.&lt;/p&gt;

&lt;p&gt;Let's consider in this article the example of a Python application whose new releases are tagged through &lt;code&gt;git&lt;/code&gt; tags, and each time a new version is released we want to create a &lt;code&gt;pip&lt;/code&gt; package and upload it to the online &lt;code&gt;PyPI&lt;/code&gt; registry.  However, you can easily adapt this example to work with any build and release process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twine.readthedocs.io/en/latest/#twine-register"&gt;Twine&lt;/a&gt; is a quite popular option when it comes to uploading packages to the &lt;code&gt;PyPI&lt;/code&gt; registry. Let's install it:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;twine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a Gitlab webhook that reacts to tag events, or react to a &lt;a href="https://docs.platypush.tech/en/latest/platypush/events/github.html#platypush.message.event.github.GithubCreateEvent"&gt;&lt;code&gt;GithubCreateEvent&lt;/code&gt;&lt;/a&gt; if you are using Github, and create a Platypush hook that reacts to tag events by running the logic of &lt;code&gt;on_repo_push&lt;/code&gt;, and additionally make a package build and upload it with Twine if the tests are successful:&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;import&lt;/span&gt; &lt;span class="nn"&gt;importlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.event.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.message.event.http.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebhookEvent&lt;/span&gt;

&lt;span class="c1"&gt;# Path where the latest version of the repo has been cloned
&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'/tmp/repo'&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize these variables with your PyPI credentials
&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;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'TWINE_USERNAME'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'your-pypi-user'&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;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'TWINE_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'your-pypi-pass'&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;upload_pip_package&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;chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Build the package
&lt;/span&gt;  &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;'python'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'setup.py'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'sdist'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'bdist_wheel'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="c1"&gt;# Check the version of your app - for example from the
&lt;/span&gt;  &lt;span class="c1"&gt;# yourapp/__init__.py __version__ field
&lt;/span&gt;  &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;import_module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'yourapp'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&lt;/span&gt;

  &lt;span class="c1"&gt;# Check that the archive file has been created
&lt;/span&gt;  &lt;span class="n"&gt;archive_file&lt;/span&gt; &lt;span class="o"&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;join&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;'dist'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'yourapp-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.tar.gz'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;assert&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;isfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; \
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'The target file &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;archive_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; was not created'&lt;/span&gt;

  &lt;span class="c1"&gt;# Upload the archive file to PyPI
&lt;/span&gt;  &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;'twine'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'upload'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebhookEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'repo-tag'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# or
# @hook(GithubCreateEvent)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_repo_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="c1"&gt;# ...
&lt;/span&gt;
  &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;upload_pip_package&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;And here you go - you now have an automated way of building and releasing your application!&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous delivery of web applications
&lt;/h2&gt;

&lt;p&gt;We have seen in this article some examples of CI/CD for stand-alone applications with a complete test+build+release pipeline. The same concept also applies to web services and applications. If your repository stores the source code of a website, then you can easily create automations that react to push events and pull the changes on the web server and restart the web service if required. This is in fact the way I'm currently managing updates on the Platypush &lt;a href="https://blog.platypush.tech/"&gt;blog&lt;/a&gt; and &lt;a href="https://platypush.tech/"&gt;homepage&lt;/a&gt;. Let's see a small example where we have a Platypush instance running on the same machine as the web server, and suppose that our website is served under &lt;code&gt;/srv/http/myapp&lt;/code&gt; (and, of course, that the user that runs the Platypush service has write permissions on this location). It's quite easy to tweak the previous hook example so that it reacts to push events on this repo by pulling the latest changes, runs e.g. &lt;code&gt;npm run build&lt;/code&gt; to build the new &lt;code&gt;dist&lt;/code&gt; files and then copies the &lt;code&gt;dist&lt;/code&gt; folder to our web server directory:&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;import&lt;/span&gt; &lt;span class="nn"&gt;os&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;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.event.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.message.event.http.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebhookEvent&lt;/span&gt;

&lt;span class="c1"&gt;# Path where the latest version of the repo has been cloned
&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'/tmp/repo'&lt;/span&gt;

&lt;span class="c1"&gt;# Path of the web application
&lt;/span&gt;&lt;span class="n"&gt;webapp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'/srv/http/myapp'&lt;/span&gt;

&lt;span class="c1"&gt;# Backup path of the web application
&lt;/span&gt;&lt;span class="n"&gt;backup_webapp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'/srv/http/myapp-backup'&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;update_webapp&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;chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Build the app
&lt;/span&gt;  &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;'npm'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'install'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;'npm'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'run'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'build'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="c1"&gt;# Verify that the dist folder has been created
&lt;/span&gt;  &lt;span class="n"&gt;dist_path&lt;/span&gt; &lt;span class="o"&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;join&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;'dist'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;assert&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;isdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dist_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;'dist path not created'&lt;/span&gt;

  &lt;span class="c1"&gt;# Remove the previous app backup folder if present
&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;backup_webapp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignore_errors&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="c1"&gt;# Backup the old web app folder
&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;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webapp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backup_webapp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Move the dist folder to the web app folder
&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;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dist_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;webapp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebhookEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'repo-push'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# or
# @hook(GithubPushEvent)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_repo_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="c1"&gt;# ...
&lt;/span&gt;
  &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;update_webapp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

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

&lt;/div&gt;



</description>
      <category>platypush</category>
      <category>ci</category>
      <category>continuousdelivery</category>
      <category>git</category>
    </item>
    <item>
      <title>Deliver customized newsletters from RSS feeds with Platypush</title>
      <dc:creator>Fabio Manganiello</dc:creator>
      <pubDate>Sun, 06 Sep 2020 21:57:15 +0000</pubDate>
      <link>https://forem.com/blacklight/deliver-customized-newsletters-from-rss-feeds-with-platypush-3mip</link>
      <guid>https://forem.com/blacklight/deliver-customized-newsletters-from-rss-feeds-with-platypush-3mip</guid>
      <description>&lt;p&gt;I've always been a fan of well-curated newsletters. They give me an opportunity to get a good overview of what happened in the fields I follow within a span of a day, a week or a month. However, not all the newsletters fit this category. Some don't think three times before selling email addresses to 3rd-parties, and within the blink of an eye, your mailbox can easily get flooded with messages that you didn't request. Others may sign up your address for other services or newsletters as well, and often they don't often much granularity to configure which communications you want to receive. Even in the best-case scenario, the most privacy-savvy user may still think twice before signing up for a newsletter - you're giving your personal email address to someone else you don't necessarily trust, implying "yes, this is my address and I'm interested in this subject". Additionally, most of the newsletters spice up their URLs with tracking parameters, so they can easily measure user engagement - something you may not necessarily be happy with. Moreover, the customization junkie may also have a use case for a more finely tuned selection of content in his newsletter - you may want to group some sources together into the same daily/weekly email, or you may be interested only in some particular subset of the subjects covered by a newsletter, filtering out those that aren't relevant, or customize the style of the digest that gets delivered. Finally, a fully automated way to deliver newsletters through 5 lines of code and the tuning of a couple of parameters is the nirvana for many companies of every size out there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feed up the newsletter
&lt;/h2&gt;

&lt;p&gt;Those who read my articles in the past may know that I'm an avid consumer of RSS feeds. Despite being a 21-year-old technology, they do their job very well when it comes to deliver the information that matters without all the noise and trackers, and they provide a very high level of integration being simple XML documents. However, in spite of all the effort I put to be up-to-date with all my sources, a lot of potentially interesting content inevitably slips through - and that's where newsletters step in, as they filter and group together all the content that was generated in a given time frame and periodically deliver it to your inbox.&lt;/p&gt;

&lt;p&gt;My ideal solution would be something that combines the best aspects of both the worlds: the flexibility of an RSS subscription, combined with a flexible way of filtering and aggregating content and sources, and get the full package delivered at my door in whichever format I like (HTML, PDF, MOBI…). In this article I'm going to show how to achieve this goal with a few tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;One or more sources that you want to track and that support RSS feeds (in this example I'll use the MIT Technology Review &lt;a href="https://www.technologyreview.com/feed/"&gt;RSS feed&lt;/a&gt;, but the procedure works for any RSS feed).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An email address.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/BlackLight/platypush"&gt;Platypush&lt;/a&gt; to do the heavy-lifting job - monitor the RSS sources at custom intervals, trigger events when a source has some new content, create a digest out of the new content, and deliver the full package to a list of email addresses.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's cover these points step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing and configuring Platypush
&lt;/h2&gt;

&lt;p&gt;Those who have already read my previous articles may have heard of &lt;a href="https://github.com/BlackLight/platypush"&gt;Platypush&lt;/a&gt; - the automation platform I've been building in the past few years. For those who aren't familiar, an advised read is my &lt;a href="https://medium.com/swlh/automate-your-house-your-life-and-everything-else-around-with-platypush-dba1cd13e3f6"&gt;first Medium post&lt;/a&gt; that illustrates some of its capabilities and the paradigm behind it.&lt;/p&gt;

&lt;p&gt;We'll be using the &lt;a href="https://platypush.readthedocs.io/en/latest/platypush/backend/http.poll.html"&gt;&lt;code&gt;http.poll&lt;/code&gt;&lt;/a&gt; backend configured with one or more &lt;a href="https://github.com/BlackLight/platypush/blob/ac02becba80fafce39d5bbcfc682f7a8fe46f529/platypush/backend/http/request/rss/__init__.py#L21"&gt;&lt;code&gt;RssUpdates&lt;/code&gt;&lt;/a&gt; objects to poll our RSS sources at regular intervals and create the digests, and either the &lt;a href="https://platypush.readthedocs.io/en/latest/platypush/plugins/mail.smtp.html"&gt;&lt;code&gt;mail.smtp&lt;/code&gt;&lt;/a&gt; plugin or the &lt;a href="https://platypush.readthedocs.io/en/latest/platypush/plugins/google.mail.html"&gt;&lt;code&gt;google.mail&lt;/code&gt;&lt;/a&gt; plugin to send the digests to our email.&lt;/p&gt;

&lt;p&gt;You can install Platypush on any device where you want to run your logic - a RaspberryPi, an old laptop, a cloud node, and so on. We will install the base package with the &lt;code&gt;rss&lt;/code&gt; module. Optionally, you can install it with the &lt;code&gt;pdf&lt;/code&gt; module as well (if you want to export your digests also to PDF) or the &lt;code&gt;google&lt;/code&gt; module (if you want to send the newsletter from a GMail address instead of an SMTP server).&lt;/p&gt;

&lt;p&gt;The first option is to install the latest stable version through &lt;code&gt;pip&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;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'platypush[rss]'&lt;/span&gt;
&lt;span class="c"&gt;# Or&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'platypush[rss,pdf,google]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other option is to install the latest git version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone git@github.com/BlackLight/platypush.git
&lt;span class="nb"&gt;cd &lt;/span&gt;platypush
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'.[rss]'&lt;/span&gt;
&lt;span class="c"&gt;# Or&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'.[rss,pdf,google]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Monitoring your RSS feeds
&lt;/h2&gt;

&lt;p&gt;Once the software is installed, create the configuration file &lt;code&gt;~/.config/platypush/config.yaml&lt;/code&gt; if it doesn't exist already and add the configuration for the RSS monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Generic HTTP endpoint monitor&lt;/span&gt;
&lt;span class="na"&gt;backend.http.poll&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Add a new RSS feed to the pool&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;platypush.backend.http.request.rss.RssUpdates&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.technologyreview.com/feed/&lt;/span&gt;  &lt;span class="c1"&gt;# URL to the RSS feed&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MIT Technology Review&lt;/span&gt;                 &lt;span class="c1"&gt;# Title of the feed (shown in the head of the digest)&lt;/span&gt;
          &lt;span class="na"&gt;poll_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt;                          &lt;span class="c1"&gt;# How often we should monitor this source (24*60*60 secs = once a day)&lt;/span&gt;
          &lt;span class="na"&gt;digest_format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;html&lt;/span&gt;                          &lt;span class="c1"&gt;# Format of the digest (HTML or PDF)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also add more sources to the &lt;code&gt;http.poll&lt;/code&gt; &lt;code&gt;requests&lt;/code&gt; object, each with its own configuration. Also, you can customize the style of your digest by passing some valid CSS to these configuration attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Style of the body element&lt;/span&gt;
&lt;span class="na"&gt;body_style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;font-size:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;20px;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;font-family:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Merriweather",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Georgia,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Times&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;New&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Roman",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Times,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;serif'&lt;/span&gt;

&lt;span class="c1"&gt;# Style of the main title&lt;/span&gt;
&lt;span class="na"&gt;title_style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;margin-top:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;30px'&lt;/span&gt;

&lt;span class="c1"&gt;# Style of the subtitle&lt;/span&gt;
&lt;span class="na"&gt;subtitle_style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;margin-top:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;10px;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;page-break-after:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;always'&lt;/span&gt;

&lt;span class="c1"&gt;# Style of the article titles&lt;/span&gt;
&lt;span class="na"&gt;article_title_style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;font-size:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1.6em;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;margin-top:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1em;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;padding-top:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1em;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;border-top:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1px&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;solid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;#999'&lt;/span&gt;

&lt;span class="c1"&gt;# Style of the article link&lt;/span&gt;
&lt;span class="na"&gt;article_link_style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;color:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;#555;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;text-decoration:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;none;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;border-bottom:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1px&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dotted&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;font-size:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0.8em'&lt;/span&gt;

&lt;span class="c1"&gt;# Style of the article content&lt;/span&gt;
&lt;span class="na"&gt;article_content_style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;font-size:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0.8em'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;digest_format&lt;/code&gt; attribute determines the output format of your digest - you may want to choose &lt;code&gt;html&lt;/code&gt; if you want to deliver a summary of the articles in a newsletter, or &lt;code&gt;pdf&lt;/code&gt; if you want instead to deliver the full content of each item as an attachment to an email address. Bonus point: since you can send PDFs to a Kindle if you &lt;a href="https://www.amazon.com/gp/sendtokindle/email"&gt;configured an email address&lt;/a&gt;, this mechanism allows you to deliver the full digest of your RSS feeds to your Kindle's email address.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/BlackLight/platypush/blob/ac02becba80fafce39d5bbcfc682f7a8fe46f529/platypush/backend/http/request/rss/__init__.py#L21"&gt;&lt;code&gt;RssUpdates&lt;/code&gt;&lt;/a&gt; object also provides native integration with the &lt;a href="https://github.com/postlight/mercury-parser-api"&gt;Mercury Parser API&lt;/a&gt; to automatically scrape the content of a web page - I covered some of these concepts in my &lt;a href="https://medium.com/better-programming/make-the-web-readable-again-how-to-automatically-deliver-article-and-news-digests-to-your-45dc69773380"&gt;past article&lt;/a&gt; on how to parse RSS feeds and send the PDF digest to your e-reader. The same mechanism works well for newsletters too. If you want to parse the content of the newsletter as well, all you have to do is configure the &lt;a href="https://platypush.readthedocs.io/en/latest/platypush/plugins/http.webpage.html"&gt;&lt;code&gt;http.webpage&lt;/code&gt;&lt;/a&gt; Platypush plugin. Since the Mercury API doesn't provide a Python binding, this requires a couple of JavaScript dependencies:&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="c"&gt;# Install Node and NPM, e.g. on Debian:&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install &lt;/span&gt;nodejs npm

&lt;span class="c"&gt;# Install the Mercury Parser API&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; @postlight/mercury-parser

&lt;span class="c"&gt;# Make sure that the Platypush PDF module dependencies&lt;/span&gt;
&lt;span class="c"&gt;# are installed if you plan HTML-&amp;gt;PDF conversion&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'platypush[pdf]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, if you want to parse the full content of the items and generate a PDF digest out of them, change your &lt;code&gt;http.poll&lt;/code&gt; configuration to something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;backend.http.poll&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;platypush.backend.http.request.rss.RssUpdates&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.technologyreview.com/feed/&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MIT Technology Review&lt;/span&gt;
          &lt;span class="na"&gt;poll_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt;
          &lt;span class="na"&gt;digest_format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pdf&lt;/span&gt;     &lt;span class="c1"&gt;# PDF digest format&lt;/span&gt;
          &lt;span class="na"&gt;extract_content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# Extract the full content of the items&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;WARNING&lt;/strong&gt;: Extracting the full content of the articles in an RSS feed has two limitations - a practical one and a legal one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Some websites may require user login before displaying the full content of an article. Some websites perform such checks client-side - and the parser API can usually circumvent them, especially if the full content of an article is actually just hidden behind a client-side paywall. Some websites, however, implement their user checks server-side too before sending the content to the client - and in those cases the parser API may return only a part of the content or no content at all.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Always keep in mind that parsing the full content of an article behind a paywall may represent a violation of intellectual property under some jurisdictions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configuring the mail delivery
&lt;/h2&gt;

&lt;p&gt;When new content is published on a subscribed RSS feed Platypush will generate a &lt;a href="https://platypush.readthedocs.io/en/latest/platypush/events/http.rss.html"&gt;&lt;code&gt;NewFeedEvent&lt;/code&gt;&lt;/a&gt; and it should create a copy of the digest under &lt;code&gt;~/.local/share/platypush/feeds/cache/{date:time}_{feed-title}.[html|pdf]&lt;/code&gt;. The &lt;code&gt;NewFeedEvent&lt;/code&gt; in particular is the link you need to create your custom logic that sends an email to a list of addresses when new content is available.&lt;/p&gt;

&lt;p&gt;First, configure the Platypush mail plugin you prefer. When it comes to sending emails you primarily have two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://platypush.readthedocs.io/en/latest/platypush/plugins/mail.smtp.html"&gt;&lt;code&gt;mail.smtp&lt;/code&gt;&lt;/a&gt; plugin - if you want to send emails directly through an SMTP server. Platypush configuration:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mail.smtp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;you@gmail.com&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-pass&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smtp.gmail.com&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;465&lt;/span&gt;
    &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://platypush.readthedocs.io/en/latest/platypush/plugins/google.mail.html"&gt;&lt;code&gt;google.mail&lt;/code&gt;&lt;/a&gt; plugin - if you want to use the native GMail API to send emails. If that is the case then first make sure that you have the dependencies for the Platypush Google module installed:
&lt;/li&gt;
&lt;/ul&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="s1"&gt;'platypush[google]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case you'll also have to create a project on the &lt;a href="https://console.developers.google.com/"&gt;Google Developers console&lt;/a&gt; and download the OAuth credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click on “Credentials” from the context menu &amp;gt; OAuth Client ID.&lt;/li&gt;
&lt;li&gt;Once generated, you can see your new credentials in the “OAuth 2.0 client IDs” section. Click on the “Download” icon to save them to a JSON file.&lt;/li&gt;
&lt;li&gt;Copy the file to your Platypush device/server under e.g. &lt;code&gt;~/.credentials/google/client_secret.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the following command on the device to authorize the application:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; platypush.plugins.google.credentials &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"https://www.googleapis.com/auth/gmail.modify"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    ~/.credentials/google/client_secret.json &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--noauth_local_webserver&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Copy the link in your browser, log in with your Google account, and authorize the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point the GMail delivery is ready to be used by your Platypush automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting the dots
&lt;/h2&gt;

&lt;p&gt;Now that both the RSS parsing logic and the mail integration are in place, we can glue them together through the &lt;a href="https://platypush.readthedocs.io/en/latest/platypush/events/http.rss.html"&gt;&lt;code&gt;NewFeedEvent&lt;/code&gt;&lt;/a&gt; event. The new advised way to configure events in Platypush is through native Python scripts - the custom YAML-based syntax for events and procedure was becoming too cumbersome to maintain and write, and I feel like going back to a clean and simple Python API may be a better option.&lt;/p&gt;

&lt;p&gt;Create and initialize the Platypush scripts directory, if it doesn't exist already:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/platypush/scripts
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/.config/platypush/scripts
&lt;span class="nb"&gt;touch &lt;/span&gt;__init__.py  &lt;span class="c"&gt;# Initialize the root Python module&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, create a new &lt;em&gt;hook&lt;/em&gt; on &lt;code&gt;NewFeedEvent&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;&lt;span class="nv"&gt;$EDITOR&lt;/span&gt; rss_news.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.event.hook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.message.event.http.rss&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NewFeedEvent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;platypush.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;

&lt;span class="c1"&gt;# Path to your mailing list - a text file with one address per line
&lt;/span&gt;&lt;span class="n"&gt;maillist&lt;/span&gt; &lt;span class="o"&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;expanduser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'~/.mail.list'&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;get_addresses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&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;maillist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'r'&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&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;addr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;startswith&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="c1"&gt;# This hook matches:
# - event_type=NewFeedEvent
# - digest_format='html'
# - source_title='MIT Technology Review'
&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewFeedEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digest_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'html'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'MIT Technology Review'&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;send_mit_rss_feed_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NewFeedEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# The digest output file is stored in event.args['digest_filename']
&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;event&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="s"&gt;'digest_filename'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s"&gt;'r'&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'mail.smtp.send'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;from_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'you@yourdomain.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_addresses&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&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="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"source_title"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; feed digest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;body_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'html'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you opted for the native GMail plugin you may want to go for:&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="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewFeedEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digest_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'html'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'MIT Technology Review'&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;send_mit_rss_feed_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NewFeedEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# The digest output file is stored in event.args['digest_filename']
&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;event&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="s"&gt;'digest_filename'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s"&gt;'r'&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'google.mail.compose'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'you@gmail.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_addresses&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&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="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"source_title"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; feed digest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If instead you want to send the digest in PDF format as an attachment:&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="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewFeedEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digest_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'html'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'MIT Technology Review'&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;send_mit_rss_feed_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NewFeedEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# mail.smtp plugin case
&lt;/span&gt;    &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'mail.smtp.send'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;from_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'you@yourdomain.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_addresses&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&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="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"source_title"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; feed digest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&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;attachments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&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="s"&gt;'digest_filename'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# google.mail case
&lt;/span&gt;    &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'google.mail.compose'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'you@gmail.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_addresses&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&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="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"source_title"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; feed digest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&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;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&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="s"&gt;'digest_filename'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, create your &lt;code&gt;~/.mail.list&lt;/code&gt; file with one destination email address per line and start &lt;code&gt;platypush&lt;/code&gt; either from the command line or as a service. You should receive your email with the first batch of articles shortly after startup, and you'll receive more items if a new batch is available after the &lt;code&gt;poll_seconds&lt;/code&gt; configured period.&lt;/p&gt;

</description>
      <category>platypush</category>
      <category>rss</category>
      <category>newsletter</category>
      <category>automation</category>
    </item>
    <item>
      <title>One browser extension to rule them all</title>
      <dc:creator>Fabio Manganiello</dc:creator>
      <pubDate>Wed, 08 Jul 2020 09:17:25 +0000</pubDate>
      <link>https://forem.com/blacklight/one-browser-extension-to-rule-them-all-5d7d</link>
      <guid>https://forem.com/blacklight/one-browser-extension-to-rule-them-all-5d7d</guid>
      <description>&lt;p&gt;The current WebExtensions API has made the world of browser extensions safer and more cross-compatible, but it has also introduced its share of limitations and compromises in order to achieve better security.&lt;/p&gt;

&lt;p&gt;Something I miss from the "first wave" of add-ons/browser apps is the possibility to customise every single aspect of the browser (UI, input events, background scripts etc.) through tweaks and custom scripts. And I've always wanted the ability to connect these tweaks/custom browser actions to my increasing (and increasingly fragmented) network of smart devices around - perform actions like turn on the lights, cast a video or action a switch directly from the browser, without grabbing the phone and opening an app, and without switching tabs in the browser. Moreover, I wanted the ability to run any kind of simple browser actions (simplify/translate page, share to Twitter/Facebook, play on Chromecast/Kodi, send link to mobile device...) as simple JavaScript snippets within the same extension - I've always considered the idea of having a separate extension, and a separate icon in the browser toolbar, just to do one specific little thing as pure madness.&lt;/p&gt;

&lt;p&gt;So I got my plan together and finally developed an extension that could fill the gap. An extension that makes e.g. writing a browser action to cast a YouTube URL to your Chromecast something as simple as a JavaScript snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getURL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.youtube.com/watch?v=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;media.chromecast.play&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YouTube video now playing on Chromecast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Medium article: &lt;a href="https://medium.com/@automationguru/one-browser-extension-to-rule-them-all-3118dc7f9c9b"&gt;https://medium.com/@automationguru/one-browser-extension-to-rule-them-all-3118dc7f9c9b&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Firefox link: &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/platypush/"&gt;https://addons.mozilla.org/en-US/firefox/addon/platypush/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chrome link: &lt;a href="https://chrome.google.com/webstore/detail/platypush/aphldjclndofhflbbdnmpejbjgomkbie?hl=en-GB&amp;amp;authuser=0"&gt;https://chrome.google.com/webstore/detail/platypush/aphldjclndofhflbbdnmpejbjgomkbie?hl=en-GB&amp;amp;authuser=0&lt;/a&gt;&lt;/p&gt;

</description>
      <category>platypush</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Build your own open dashboard to keep track of COVID-19</title>
      <dc:creator>Fabio Manganiello</dc:creator>
      <pubDate>Sun, 03 May 2020 17:39:18 +0000</pubDate>
      <link>https://forem.com/blacklight/build-your-own-open-dashboard-to-keep-track-of-covid-19-n7d</link>
      <guid>https://forem.com/blacklight/build-your-own-open-dashboard-to-keep-track-of-covid-19-n7d</guid>
      <description>&lt;p&gt;&lt;a href="https://towardsdatascience.com/build-your-own-open-dashboard-to-keep-track-of-covid-19-f2b09e817320?source=rss-a6a7a8202c31------2"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OdY2GI9U--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1828/1%2A1WfU6ZFoKLlviRN7drPx-Q.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re living in relatively unprecedented times, as none of those alive today (except the few that were around in 1918) have gone through a…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://towardsdatascience.com/build-your-own-open-dashboard-to-keep-track-of-covid-19-f2b09e817320?source=rss-a6a7a8202c31------2"&gt;Continue reading on Towards Data Science »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dashboard</category>
      <category>python</category>
      <category>covid19</category>
      <category>platypush</category>
    </item>
    <item>
      <title>Building Custom Voice Assistants</title>
      <dc:creator>Fabio Manganiello</dc:creator>
      <pubDate>Sun, 08 Mar 2020 18:41:33 +0000</pubDate>
      <link>https://forem.com/blacklight/building-custom-voice-assistants-4e0e</link>
      <guid>https://forem.com/blacklight/building-custom-voice-assistants-4e0e</guid>
      <description>&lt;p&gt;&lt;a href="https://medium.com/better-programming/building-your-custom-voice-assistants-an-overview-of-the-current-solutions-and-integrations-d8db227a325?source=rss-a6a7a8202c31------2"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OmIkzkk0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2600/1%2ACUQ_QC5dWio8XiNpS95lxw.jpeg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An overview of current solutions and integrations&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/better-programming/building-your-custom-voice-assistants-an-overview-of-the-current-solutions-and-integrations-d8db227a325?source=rss-a6a7a8202c31------2"&gt;Continue reading on Better Programming »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>voiceassistant</category>
      <category>programming</category>
      <category>googleassistant</category>
      <category>diy</category>
    </item>
  </channel>
</rss>
