Skip to content

Commit

Permalink
Updated and fixed the tutorials
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm committed May 1, 2024
1 parent 434800d commit 26ac102
Show file tree
Hide file tree
Showing 13 changed files with 289 additions and 323 deletions.
91 changes: 18 additions & 73 deletions docs/tutorials/echo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ configuration file.
Prerequisites
-------------

Asphalt requires Python 3.9 or later. You will also need to have the ``venv`` module
Asphalt requires Python 3.8 or later. You will also need to have the ``venv`` module
installed for your Python version of choice. It should come with most Python
distributions, but if it does not, you can usually install it with your operating
system's package manager (``python3-venv`` is a good guess).
Expand Down Expand Up @@ -68,26 +68,18 @@ Creating the first component
----------------------------

Now, let's write some code! Create a file named ``server.py`` in the ``echo`` package
directory::
directory:

import anyio

from asphalt.core import Component, run_application


class ServerComponent(Component):
async def start(self) -> None:
print("Hello, world!")

if __name__ == "__main__":
component = ServerComponent()
run_application(component)
.. literalinclude:: snippets/echo1.py
:language: python
:start-after: isort: off

The ``ServerComponent`` class is the *root component* (and in this case, the only
component) of this application. Its ``start()`` method is called by ``run_application``
when it has set up the event loop. Finally, the ``if __name__ == '__main__':`` block is
not strictly necessary but is good, common practice that prevents ``run_application()``
from being called again if this module is ever imported from another module.
component) of this application. Its ``start()`` method is called by
:func:`run_application` when it has set up the event loop. Finally, the
``if __name__ == '__main__':`` block is not strictly necessary but is good, common
practice that prevents :func:`run_application()` from being called again if this module
is ever imported from another module.

You can now try running the above application. With the project directory
(``tutorial``) as your current directory, do:
Expand All @@ -103,39 +95,11 @@ Making the server listen for connections
----------------------------------------

The next step is to make the server actually accept incoming connections.
For this purpose, we will use AnyIO's :func:`~anyio.create_tcp_listener` function::

from collections.abc import AsyncIterator

import anyio
from anyio.abc import SocketStream

from asphalt.core import (
Component,
context_teardown,
run_application,
start_background_task,
)


async def handle(stream: SocketStream) -> None:
message = await stream.receive()
await stream.send(message)
print("Message from client:", message.decode().rstrip())
For this purpose, we will use AnyIO's :func:`~anyio.create_tcp_listener` function:


class ServerComponent(Component):
@context_teardown
async def start(self) -> AsyncGenerator[None, Exception | None]:
async with await anyio.create_tcp_listener(
local_host="localhost", local_port=64100
) as listener:
start_background_task(lambda: listener.serve(handle), "Echo server")
yield

if __name__ == '__main__':
component = ServerComponent()
run_application(component)
.. literalinclude:: ../../examples/tutorial1/echo/server.py
:language: python
:start-after: isort: off

Here, :func:`anyio.create_tcp_listener` is used to listen to incoming TCP connections on
the ``localhost`` interface on port 64100. The port number is totally arbitrary and can
Expand Down Expand Up @@ -175,30 +139,11 @@ No server is very useful without a client to access it, so we'll need to add a c
module in this project. And to make things a bit more interesting, we'll make the client
accept a message to be sent as a command line argument.

Create the file ``client.py`` file in the ``echo`` package directory as follows::

import sys

import anyio

from asphalt.core import CLIApplicationComponent, run_application


class ClientComponent(CLIApplicationComponent):
def __init__(self, message: str):
super().__init__()
self.message = message

async def run(self) -> None:
async with await anyio.connect_tcp("localhost", 64100) as stream:
await stream.send(self.message.encode() + b"\n")
response = await stream.receive()

print("Server responded:", response.decode().rstrip())
Create the file ``client.py`` file in the ``echo`` package directory as follows:

if __name__ == '__main__':
component = ClientComponent(sys.argv[1])
run_application(component)
.. literalinclude:: ../../examples/tutorial1/echo/client.py
:language: python
:start-after: isort: off

You may have noticed that ``ClientComponent`` inherits from
:class:`CLIApplicationComponent` instead of :class:`Component` and that instead of
Expand Down
14 changes: 14 additions & 0 deletions docs/tutorials/snippets/echo1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# isort: off
from __future__ import annotations

from asphalt.core import Component, run_application


class ServerComponent(Component):
async def start(self) -> None:
print("Hello, world!")


if __name__ == "__main__":
component = ServerComponent()
run_application(component)
20 changes: 20 additions & 0 deletions docs/tutorials/snippets/webnotifier-app1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# isort: off
import logging

import anyio
import httpx
from asphalt.core import CLIApplicationComponent, run_application

logger = logging.getLogger(__name__)


class ApplicationComponent(CLIApplicationComponent):
async def run(self) -> None:
async with httpx.AsyncClient() as http:
while True:
await http.get("https://imgur.com")
await anyio.sleep(10)


if __name__ == "__main__":
run_application(ApplicationComponent(), logging=logging.DEBUG)
32 changes: 32 additions & 0 deletions docs/tutorials/snippets/webnotifier-app2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# isort: off
from __future__ import annotations

import logging
from typing import Any

import anyio
import httpx
from asphalt.core import CLIApplicationComponent, run_application

logger = logging.getLogger(__name__)


class ApplicationComponent(CLIApplicationComponent):
async def run(self) -> None:
last_modified = None
async with httpx.AsyncClient() as http:
while True:
headers: dict[str, Any] = (
{"if-modified-since": last_modified} if last_modified else {}
)
response = await http.get("https://imgur.com", headers=headers)
logger.debug("Response status: %d", response.status_code)
if response.status_code == 200:
last_modified = response.headers["date"]
logger.info("Contents changed")

await anyio.sleep(10)


if __name__ == "__main__":
run_application(ApplicationComponent(), logging=logging.DEBUG)
39 changes: 39 additions & 0 deletions docs/tutorials/snippets/webnotifier-app3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# isort: off
from __future__ import annotations

import logging
from difflib import unified_diff
from typing import Any

import anyio
import httpx
from asphalt.core import CLIApplicationComponent, run_application

logger = logging.getLogger(__name__)


class ApplicationComponent(CLIApplicationComponent):
async def run(self) -> None:
async with httpx.AsyncClient() as http:
last_modified, old_lines = None, None
while True:
logger.debug("Fetching webpage")
headers: dict[str, Any] = (
{"if-modified-since": last_modified} if last_modified else {}
)
response = await http.get("https://imgur.com", headers=headers)
logger.debug("Response status: %d", response.status_code)
if response.status_code == 200:
last_modified = response.headers["date"]
new_lines = response.text.split("\n")
if old_lines is not None and old_lines != new_lines:
difference = unified_diff(old_lines, new_lines)
logger.info("Contents changed:\n%s", difference)

old_lines = new_lines

await anyio.sleep(10)


if __name__ == "__main__":
run_application(ApplicationComponent(), logging=logging.DEBUG)
55 changes: 55 additions & 0 deletions docs/tutorials/snippets/webnotifier-app4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# isort: off
from __future__ import annotations

import logging
from difflib import HtmlDiff
from typing import Any

import anyio
import httpx
from asphalt.core import CLIApplicationComponent, run_application
from asphalt.core import inject, resource
from asphalt.mailer import Mailer

logger = logging.getLogger(__name__)


class ApplicationComponent(CLIApplicationComponent):
async def start(self) -> None:
self.add_component(
"mailer",
backend="smtp",
host="your.smtp.server.here",
message_defaults={"sender": "[email protected]", "to": "[email protected]"},
)
await super().start()

@inject
async def run(self, *, mailer: Mailer = resource()) -> None:
async with httpx.AsyncClient() as http:
last_modified, old_lines = None, None
diff = HtmlDiff()
while True:
logger.debug("Fetching webpage")
headers: dict[str, Any] = (
{"if-modified-since": last_modified} if last_modified else {}
)
response = await http.get("https://imgur.com", headers=headers)
logger.debug("Response status: %d", response.status_code)
if response.status_code == 200:
last_modified = response.headers["date"]
new_lines = response.text.split("\n")
if old_lines is not None and old_lines != new_lines:
difference = diff.make_file(old_lines, new_lines, context=True)
await mailer.create_and_deliver(
subject="Change detected in web page", html_body=difference
)
logger.info("Sent notification email")

old_lines = new_lines

await anyio.sleep(10)


if __name__ == "__main__":
run_application(ApplicationComponent(), logging=logging.DEBUG)
12 changes: 12 additions & 0 deletions docs/tutorials/snippets/webnotifier-detector1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# isort: off
from __future__ import annotations

from dataclasses import dataclass

from asphalt.core import Event


@dataclass
class WebPageChangeEvent(Event):
old_lines: list[str]
new_lines: list[str]
47 changes: 47 additions & 0 deletions docs/tutorials/snippets/webnotifier-detector2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# isort: off
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import Any

import anyio
import httpx

from asphalt.core import Event, Signal

logger = logging.getLogger(__name__)


@dataclass
class WebPageChangeEvent(Event):
old_lines: list[str]
new_lines: list[str]


class Detector:
changed = Signal(WebPageChangeEvent)

def __init__(self, url: str, delay: float):
self.url = url
self.delay = delay

async def run(self) -> None:
async with httpx.AsyncClient() as http:
last_modified, old_lines = None, None
while True:
logger.debug("Fetching contents of %s", self.url)
headers: dict[str, Any] = (
{"if-modified-since": last_modified} if last_modified else {}
)
response = await http.get("https://imgur.com", headers=headers)
logger.debug("Response status: %d", response.status_code)
if response.status_code == 200:
last_modified = response.headers["date"]
new_lines = response.text.split("\n")
if old_lines is not None and old_lines != new_lines:
self.changed.dispatch(WebPageChangeEvent(old_lines, new_lines))

old_lines = new_lines

await anyio.sleep(self.delay)
Loading

0 comments on commit 26ac102

Please sign in to comment.