From 9e5814ed0f0ebdd6e14d4e46f84957b3b8dded6f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 1 Feb 2023 16:22:14 +0100 Subject: [PATCH 01/17] Strip improvements and line api --- src/textual/_styles_cache.py | 5 +++-- src/textual/strip.py | 24 +++++++++++++++++++++--- src/textual/widgets/_text_log.py | 1 - 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/textual/_styles_cache.py b/src/textual/_styles_cache.py index a874b287b..dea21a3fb 100644 --- a/src/textual/_styles_cache.py +++ b/src/textual/_styles_cache.py @@ -10,7 +10,7 @@ from rich.style import Style from ._border import get_box, render_row from ._filter import LineFilter from ._opacity import _apply_opacity -from ._segment_tools import line_crop, line_pad, line_trim +from ._segment_tools import line_pad, line_trim from ._typing import TypeAlias from .color import Color from .geometry import Region, Size, Spacing @@ -22,7 +22,7 @@ if TYPE_CHECKING: from .css.styles import StylesBase from .widget import Widget -RenderLineCallback: TypeAlias = Callable[[int], List[Segment]] +RenderLineCallback: TypeAlias = Callable[[int], Strip] @lru_cache(1024 * 8) @@ -313,6 +313,7 @@ class StylesCache: content_y = y - gutter.top if content_y < content_height: line = render_content_line(y - gutter.top) + line = line.adjust_cell_length(content_width) else: line = [make_blank(content_width, inner)] if inner: diff --git a/src/textual/strip.py b/src/textual/strip.py index 95cf03038..c10a649a9 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -6,7 +6,7 @@ from typing import Iterable, Iterator import rich.repr from rich.cells import cell_len, set_cell_size from rich.segment import Segment -from rich.style import Style +from rich.style import Style, StyleType from ._cache import FIFOCache from ._filter import LineFilter @@ -49,7 +49,7 @@ class Strip: return "".join(segment.text for segment in self._segments) @classmethod - def blank(cls, cell_length: int, style: Style | None) -> Strip: + def blank(cls, cell_length: int, style: StyleType | None = None) -> Strip: """Create a blank strip. Args: @@ -59,7 +59,8 @@ class Strip: Returns: New strip. """ - return cls([Segment(" " * cell_length, style)], cell_length) + segment_style = Style.parse(style) if isinstance(style, str) else style + return cls([Segment(" " * cell_length, segment_style)], cell_length) @classmethod def from_lines( @@ -135,6 +136,23 @@ class Strip: self._segments == strip._segments and self.cell_length == strip.cell_length ) + def extend_cell_length(self, cell_length: int, style: Style | None = None) -> Strip: + """Extend the cell length if it is less than the given value. + + Args: + cell_length: Required minimum cell length. + style: Style for padding if the cell length is extended. + + Returns: + A new Strip. + """ + if self.cell_length < cell_length: + missing_space = cell_length - self.cell_length + segments = self._segments + [Segment(" " * missing_space, style)] + return Strip(segments, cell_length) + else: + return self + def adjust_cell_length(self, cell_length: int, style: Style | None = None) -> Strip: """Adjust the cell length, possibly truncating or extending. diff --git a/src/textual/widgets/_text_log.py b/src/textual/widgets/_text_log.py index 1c8ad17a0..3b6f931f4 100644 --- a/src/textual/widgets/_text_log.py +++ b/src/textual/widgets/_text_log.py @@ -14,7 +14,6 @@ from ..reactive import var from ..geometry import Size, Region from ..scroll_view import ScrollView from .._cache import LRUCache -from .._segment_tools import line_crop from ..strip import Strip From 0a5838d9643e80b1bd80232c97123f758a973513 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 1 Feb 2023 16:26:55 +0100 Subject: [PATCH 02/17] checker example --- docs/examples/guide/checker.py | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/examples/guide/checker.py diff --git a/docs/examples/guide/checker.py b/docs/examples/guide/checker.py new file mode 100644 index 000000000..27419bf96 --- /dev/null +++ b/docs/examples/guide/checker.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from textual.app import App, ComposeResult +from textual.geometry import Size +from textual.strip import Strip +from textual.scroll_view import ScrollView + +from rich.segment import Segment + + +class CheckerBoard(ScrollView): + COMPONENT_CLASSES = { + "checkerboard--white-square", + "checkerboard--black-square", + "checkerboard--void", + } + + DEFAULT_CSS = """ + CheckerBoard { + background: $primary; + } + CheckerBoard .checkerboard--void { + background: $background; + } + + CheckerBoard .checkerboard--white-square { + background: $foreground 70%; + } + CheckerBoard .checkerboard--black-square { + background: $primary; + } + """ + + def on_mount(self) -> None: + self.virtual_size = Size(64, 32) + + def render_line(self, y: int) -> Strip: + """Render a line of the widget. y is relative to the top of the widget.""" + + scroll_x, scroll_y = self.scroll_offset + y += scroll_y + row_index = y // 4 # four lines per row + + white = self.get_component_rich_style("checkerboard--white-square") + black = self.get_component_rich_style("checkerboard--black-square") + void = self.get_component_rich_style("checkerboard--void") + + if row_index >= 8: + return Strip.blank(self.size.width, void) + + is_odd = row_index % 2 + + segments = [ + Segment(" " * 8, black if (column + is_odd) % 2 else white) + for column in range(8) + ] + strip = Strip(segments, 8 * 8) + strip = strip.extend_cell_length(self.size.width, void) + strip = strip.crop(scroll_x, scroll_x + self.size.width) + return strip + + +class BoardApp(App): + def compose(self) -> ComposeResult: + yield CheckerBoard() + + +if __name__ == "__main__": + app = BoardApp() + app.run() From 806c80b8fe921e8cd85200c82c9947c20b41847c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 1 Feb 2023 17:14:53 +0100 Subject: [PATCH 03/17] simplify --- docs/examples/guide/checker.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/examples/guide/checker.py b/docs/examples/guide/checker.py index 27419bf96..73093d180 100644 --- a/docs/examples/guide/checker.py +++ b/docs/examples/guide/checker.py @@ -12,17 +12,12 @@ class CheckerBoard(ScrollView): COMPONENT_CLASSES = { "checkerboard--white-square", "checkerboard--black-square", - "checkerboard--void", } DEFAULT_CSS = """ CheckerBoard { background: $primary; } - CheckerBoard .checkerboard--void { - background: $background; - } - CheckerBoard .checkerboard--white-square { background: $foreground 70%; } @@ -43,10 +38,9 @@ class CheckerBoard(ScrollView): white = self.get_component_rich_style("checkerboard--white-square") black = self.get_component_rich_style("checkerboard--black-square") - void = self.get_component_rich_style("checkerboard--void") if row_index >= 8: - return Strip.blank(self.size.width, void) + return Strip.blank(self.size.width) is_odd = row_index % 2 @@ -55,7 +49,6 @@ class CheckerBoard(ScrollView): for column in range(8) ] strip = Strip(segments, 8 * 8) - strip = strip.extend_cell_length(self.size.width, void) strip = strip.crop(scroll_x, scroll_x + self.size.width) return strip From 2ff278874b7b9081e0f513e1f845b302ded49eda Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 3 Feb 2023 11:23:14 +0100 Subject: [PATCH 04/17] docs examples and diagrams --- docs/api/strip.md | 1 + docs/examples/guide/widgets/checker01.py | 43 + docs/examples/guide/widgets/checker02.py | 63 + .../{checker.py => widgets/checker03.py} | 13 +- docs/guide/widgets.md | 76 +- docs/images/render_line.excalidraw.svg | 16 + docs/images/segment.excalidraw.svg | 16 + mkdocs.yml | 1 + poetry.lock | 2307 +++++++++-------- pyproject.toml | 2 +- src/textual/_animator.py | 1 - src/textual/_parser.py | 2 - src/textual/_sleep.py | 1 - src/textual/_xterm_parser.py | 2 - src/textual/cli/previews/colors.py | 2 - src/textual/css/_styles_builder.py | 1 - src/textual/css/parse.py | 1 - src/textual/css/query.py | 1 - src/textual/css/styles.py | 1 - src/textual/devtools/service.py | 1 - src/textual/drivers/linux_driver.py | 3 - src/textual/drivers/windows_driver.py | 1 - src/textual/layouts/horizontal.py | 1 - src/textual/layouts/vertical.py | 1 - src/textual/message_pump.py | 1 - src/textual/reactive.py | 1 - src/textual/scrollbar.py | 2 - src/textual/widgets/_directory_tree.py | 1 - src/textual/widgets/_input.py | 1 - src/textual/widgets/_static.py | 1 - src/textual/widgets/_text_log.py | 1 - src/textual/widgets/_tree.py | 1 - src/textual/widgets/_welcome.py | 5 +- 33 files changed, 1381 insertions(+), 1190 deletions(-) create mode 100644 docs/api/strip.md create mode 100644 docs/examples/guide/widgets/checker01.py create mode 100644 docs/examples/guide/widgets/checker02.py rename docs/examples/guide/{checker.py => widgets/checker03.py} (85%) create mode 100644 docs/images/render_line.excalidraw.svg create mode 100644 docs/images/segment.excalidraw.svg diff --git a/docs/api/strip.md b/docs/api/strip.md new file mode 100644 index 000000000..44051a14a --- /dev/null +++ b/docs/api/strip.md @@ -0,0 +1 @@ +::: textual.strip.Strip diff --git a/docs/examples/guide/widgets/checker01.py b/docs/examples/guide/widgets/checker01.py new file mode 100644 index 000000000..66b6a0963 --- /dev/null +++ b/docs/examples/guide/widgets/checker01.py @@ -0,0 +1,43 @@ +from rich.segment import Segment +from rich.style import Style + +from textual.app import App, ComposeResult +from textual.strip import Strip +from textual.widget import Widget + + +class CheckerBoard(Widget): + """Render an 8x8 checkerboard.""" + + def render_line(self, y: int) -> Strip: + """Render a line of the widget. y is relative to the top of the widget.""" + + row_index = y // 4 # A checkerboard square consists of 4 rows + + if row_index >= 8: # Generate blank lines when we reach the end + return Strip.blank(self.size.width) + + is_odd = row_index % 2 # Used to alternate the starting square on each row + + white = Style.parse("on white") # Get a style object for a white background + black = Style.parse("on black") # Get a style object for a black background + + # Generate a list of segments with alternating black and white space characters + segments = [ + Segment(" " * 8, black if (column + is_odd) % 2 else white) + for column in range(8) + ] + strip = Strip(segments, 8 * 8) + return strip + + +class BoardApp(App): + """A simple app to show our widget.""" + + def compose(self) -> ComposeResult: + yield CheckerBoard() + + +if __name__ == "__main__": + app = BoardApp() + app.run() diff --git a/docs/examples/guide/widgets/checker02.py b/docs/examples/guide/widgets/checker02.py new file mode 100644 index 000000000..48b674ea9 --- /dev/null +++ b/docs/examples/guide/widgets/checker02.py @@ -0,0 +1,63 @@ +from rich.segment import Segment +from rich.style import Style + +from textual.app import App, ComposeResult +from textual.geometry import Size +from textual.strip import Strip +from textual.widget import Widget + + +class CheckerBoard(Widget): + """Render an 8x8 checkerboard.""" + + COMPONENT_CLASSES = { + "checkerboard--white-square", + "checkerboard--black-square", + } + + DEFAULT_CSS = """ + CheckerBoard .checkerboard--white-square { + background: #A5BAC9; + } + CheckerBoard .checkerboard--black-square { + background: #004578; + } + """ + + def get_content_width(self, container: Size, viewport: Size) -> int: + return 64 + + def get_content_height(self, container: Size, viewport: Size, width: int) -> int: + return 32 + + def render_line(self, y: int) -> Strip: + """Render a line of the widget. y is relative to the top of the widget.""" + + row_index = y // 4 # four lines per row + + if row_index >= 8: + return Strip.blank(self.size.width) + + is_odd = row_index % 2 + + white = self.get_component_rich_style("checkerboard--white-square") + black = self.get_component_rich_style("checkerboard--black-square") + + segments = [ + Segment(" " * 8, black if (column + is_odd) % 2 else white) + for column in range(8) + ] + strip = Strip(segments, 8 * 8) + return strip + + +class BoardApp(App): + """A simple app to show our widget.""" + + def compose(self) -> ComposeResult: + yield CheckerBoard() + + +if __name__ == "__main__": + app = BoardApp() + app.run() diff --git a/docs/examples/guide/checker.py b/docs/examples/guide/widgets/checker03.py similarity index 85% rename from docs/examples/guide/checker.py rename to docs/examples/guide/widgets/checker03.py index 73093d180..ccdab8ebf 100644 --- a/docs/examples/guide/checker.py +++ b/docs/examples/guide/widgets/checker03.py @@ -15,17 +15,20 @@ class CheckerBoard(ScrollView): } DEFAULT_CSS = """ - CheckerBoard { - background: $primary; - } CheckerBoard .checkerboard--white-square { - background: $foreground 70%; + background: #A5BAC9; } CheckerBoard .checkerboard--black-square { - background: $primary; + background: #004578; } """ + def get_content_width(self, container: Size, viewport: Size) -> int: + return 64 + + def get_content_height(self, container: Size, viewport: Size, width: int) -> int: + return 32 + def on_mount(self) -> None: self.virtual_size = Size(64, 32) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index d30e30c96..be7098232 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -200,4 +200,78 @@ TODO: Explanation of compound widgets ## Line API -TODO: Explanation of line API +Working with Rich renderables allows you to build sophisticated widgets with minimal effort, but there is a downside to widgets that return renderables. +When you resize a widget or update its state, Textual has to refresh the widget's content in its entirety, which may be expensive. +You are unlikely to notice this if the widget fits within the screen but large widgets that scroll may slow down your application. + +Textual offers an alternative API which reduces the amount of work Textual needs to do to refresh a widget, and makes it possible to update portions of a widget (as small as a single character). This is known as the *line API*. + +!!! info + + The [DataTable](./../widgets/data_table.md) widget uses the Line API, which can support thousands or even millions of rows without a reduction in render times. + +### Render Line method + +To build an widget with the line API, implement a `render_line` method rather than a `render` method. The `render_line` method takes a single integer argument `y` which is an offset from the top of the widget, and should return a [Strip][textual.strip.Strip] object which contains that line's content. +Textual will call this method as required to to get the content for every line. + +
+--8<-- "docs/images/render_line.excalidraw.svg" +
+ +Let's look at an example before we go in to the details. The following Textual app implements a widget with the line API that renders a checkerboard pattern. This might form the basis of a chess / checkers app. Here's the code: + +=== "checker01.py" + + ```python title="checker01.py" hl_lines="12-30" + --8<-- "docs/examples/guide/widgets/checker01.py" + ``` + +=== "Output" + + ```{.textual path="docs/examples/guide/widgets/checker01.py"} + ``` + + +The `render_line` method above calculates a `Strip` for every row of characters in the widget. Each strip contains alternating black and white space characters which form the squares in the checkerboard. You may have noticed that the checkerboard widget makes use of some objects we haven't covered before. Let's explore those. + +#### Segment and Style + +A [Segment](https://rich.readthedocs.io/en/latest/protocol.html#low-level-render) is a class borrowed from the [Rich](https://github.com/Textualize/rich) project. It is small object (actually a named tuple) which bundles text and a [Style](https://rich.readthedocs.io/en/latest/style.html) which tells Textual how the text should be displayed. + +Lets look at a simple segment which would produce the text "Hello, World!" in bold. + +```python +greeting = Segment("Hello, World!", Style(bold=True)) +``` + +This would create the following object: + +
+--8<-- "docs/images/segment.excalidraw.svg" +
+ +Both Rich and Textual work with segments to generate content. A Textual app is the result of processing hundreds, or perhaps thousands of segments. + +#### Strips + +A [Strip][textual.strip.Strip] is a container for a number of segments which define the content for a single *line* (or row) in the Widget. A Strip only requires a single segment, but will likely contain many more. + +You construct a strip with a list of segments. Here's now you might construct a strip that ultimately displays the text "Hello, World!", but with the second word in bold: + +```python +segments = [ + Segment("Hello, "), + Segment("World", Style(bold=Trip)), + Segment("!") +] +strip = Strip(segments) +``` + +The `Strip` constructor has a second optional constructor, which should be the length of the strip. In the code above, the length of the strip is 13, so we could have constructed it like this: + +```python +strip = Strip(segments, 13) +``` + +Note that the length parameter is _not_ the total number of characters in the string. It is the number of terminal "cells". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to leave the length parameter blank. diff --git a/docs/images/render_line.excalidraw.svg b/docs/images/render_line.excalidraw.svg new file mode 100644 index 000000000..0fe5edfa2 --- /dev/null +++ b/docs/images/render_line.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlT29hcdTAwMTL9nl9BMV/mVVxymtt9107V1CsggVx1MDAxMFx1MDAxMpZcdTAwMDAhydRcdTAwMTQlbNkoyFx1MDAwYrZYp/LfX1+HWPJcIi9gg/Mq/sCixbq6Oqf79HL174ulpeX0tlx1MDAxOS2/XFxajm5KYVx1MDAxMpdb4fXyXHUwMDFmfvtV1GrHjTrvws7/7cZlq9Q58ixNm+2Xf/5ZXHUwMDBiW+dR2kzCUlx1MDAxNFxcxe3LMGmnl+W4XHUwMDExlFx1MDAxYbU/4zSqtf/rf+6EteivZqNWTltBdpGVqFx1MDAxY6eN1vdrRUlUi+ppm7/9b/5/aenfzs/c6FpRKVxy69Uk6pzQ2ZVcctBcdTAwMDD1b91p1DuDNUaRVcaZ7lx1MDAwMXH7XHUwMDE1Xy6Nyry3wkOOsj1+07K+Ofp8eHXS3LFHt3fHn1x1MDAwZrU6KWN21UqcJFx1MDAwN+lt8n0mwtLZZSs3pnbaapxHx3E5Pfsxcbnt3fPaXHKehOysVuOyelaP2v7+obu10VxmS3F6y9uc6G78Plx1MDAwNy+Xsi03/N+KXHUwMDBlSElcco60sJZcdTAwMDCzK/vzQcuAJFx1MDAxOGVRXHUwMDE4YbTqXHUwMDFi13oj4SfB4/pccpxR4Wk2stOwdF7l4dXL3WPSVlhvN8NcdTAwMTY/r+y46/s7llZcdTAwMDYoJKDu7jqL4upZ6mdcdTAwMDNNoDU5LY0mcoQ2XHUwMDFiRtR5XHUwMDFjnVx1MDAxMaIlm92dv3hzq9xBxj/5XHSrl+8nrH6ZJNl4/Y7XOTRl51xcNsthen9cdTAwMTmrXHUwMDE136mTaDNcXCRx/bz/65JG6TxcdTAwMDNKZ+u3P1x1MDAxZVx1MDAwMFBcdTAwMTAgi1x1MDAxMOo0II9mXG6EJudvL07OWttm//VeYuBcXO3i7UZcdTAwMDFCS61Gu71yXHUwMDE2pqWzeaNcdTAwMTTEWJiSXGLIXGJGoWVcdTAwMWOCQtlcdTAwMDNTS1x1MDAwMVx1MDAxMfFUOKl4PnJT1lx1MDAwZlPR+TxcdTAwMWOmWqnASJRKXG5cclx1MDAwMEPAKo1cYlx1MDAxY1xiUspITyw0XHUwMDAzYJWkXHUwMDE5SFK7p1x1MDAwMKtcdTAwMDRcdTAwMGLOPlx0WG3OevRhXHUwMDE1XHUwMDE0gHDOkptcdTAwMTiselx1MDAxYtdLlZ3rXHUwMDEwV48/X8sqnCTtXCJz2lx1MDAwN7jngylcdTAwMDRsXHUwMDFhpLFCailcdTAwMTBcdTAwMDZgqlx1MDAxYym0qK11QPOEqVxmXHUwMDA0XHUwMDE4jU5bYLPoXHUwMDA2cYouMEYwX4xAI1x1MDAxOdODOCWpjVNcdTAwMDaexKiyXHUwMDAxZ/czK5xGSVx1MDAxMjfbw32+1kUoZSdI2mmaXHUwMDE4o/tblavt2/btu8b5fvpqq7Xd3jy7eFxiRuHpMOpRKEGxXHQzXHUwMDBlnKBejDpcbrRlMEhEIZxcdTAwMTbFXHUwMDFlf1x1MDAwMoz+Vlx0NWpcdTAwMWPEJ8hAXHRETVx1MDAwNvlcdTAwMTfbbDVcYlDAQHtcdTAwMTHGRlSglKhcdTAwMDa8PqNcdTAwMDadcvIp8GlJSmFmZkdH4NOKQk1cbkDEXCIsr1x0xiFcdTAwMTRL9epd8ql8UD9z7037IPz62Vx1MDAxNrn8RUGo5icvhLLOgkFgZ9mLUFx1MDAxNzAkrHYktGJcdTAwMDSbR1wi9FRcYj0vhEq2v1Yw1/6/XHUwMDEwalxuNSmwN0GSrOQnRujx3vnd3seztUR8NDtbu69cdTAwMGU+hSBcdTAwMTdcdTAwMWOhqFx1MDAwMmWV0lx1MDAxYVxmgGFcZuheiNrAXG6Bjkhaw/B4lKP/XHLwlEXtvCDK4yNhnTA/XHUwMDFmREeKUeegXGKkXHUwMDE2LT9cdTAwMWE1ReBUPtjcK5feXHUwMDFkXHUwMDFlfbXXl+9vtnVaufo4U4yWw/ZZVFx1MDAwMFLxIJCKgE2nZFx1MDAwNUhKWFx1MDAwZVVcdTAwMTimPSBcdTAwMDVNXHUwMDAxKFwiRFx1MDAwNrHkR6NcdTAwMWaFUsviqYI4xNn7iEhbdJKU4YB1aIRvOIBTqLS0SvDPXHUwMDFjlH9YUmUsx3dmXHUwMDFhmP5cdTAwMDBMXHUwMDA2XHUwMDE5eb/lWzF6u+dkZ3dh8OHi6vqotH1z/mXjjbn7dLu+d4U33Vx1MDAxYu7BZthqNa6Xu3u+3f81Wlx1MDAwMIOUs+JGXHUwMDFh3aTDaJFPXFz0225kuYfscyfnxWf68O44OXh9VHVrlcu1la33unT4sJTXXHUwMDEzWm/JXHUwMDAyg8VcdTAwMDOLXHUwMDBidD5Igj5eIFx1MDAwN/DGXHUwMDAy81x1MDAwNlBcdTAwMDL1x4+zXHUwMDBi0zCXx8i4oPuxzyzV3tnM0EY/XHUwMDE3yontP1x1MDAxYpspUJ6BqVFPXHUwMDBm4jt/XVx1MDAxND1bN8JanNz24KFcdTAwMDP/l52ZrkZpwNNfjlonfLXo99u/xH/yj6xcdTAwMWT5zf5s13P6alx1MDAxMlc9Y5aTqNJLpTQuhUl3d9poZntLPJyQv661Ve6/rUYrrsb1MDlcdTAwMWMztFx1MDAwN9FaymJak2GpLJCySVx1MDAxYpt6OSyJ7Z3j2Lb23Ea7XHUwMDE27tLm262fgNZcdTAwMTAwY31cdTAwMTJcdTAwMTmlQsh8yG1cdTAwMDczLJfY3Vx1MDAxOWFcYpx1OVx1MDAxMM6c1sPygoO0Zt2Filx1MDAxZF5mX+bJ6ovSOl1cdTAwMWVcXFx1MDAxY5TIJW+2w8r51sX705mxmvWFy+jzLKyGxWU1PJDVXG5HOGtgXHUwMDBmpmQutFx1MDAxY5v9t1W8S/ejq4+7a+vxp0q8sofXP1x1MDAwMatZPXJcdTAwMWOtOeBHpjj1hlrIpFx1MDAwNyeNXHUwMDE2IDjMcY9TsWO89ZBcdTAwMDBriLeWRGBYwj5ccq9cdTAwMWKlL2Hrc/XNUby3c0tcdTAwMDf753Sxvzs7XrOdnCYpO1x1MDAwN17j4vJcdTAwMWHH8Pr7hFx1MDAwZiE22Fx1MDAxMcxmLDsjlZ6c2aNcdTAwMTXbojJcdTAwMWJQXHUwMDA1TGpcdIZcdTAwMWShhlxcKb7zXHUwMDA1xlx1MDAwNEZIjkhAXHUwMDE4a3N7Z81rllxmgZIojbLaKeNy5eUuzW3AY+DwXHUwMDEzUbAlUjY3mlx1MDAxZlx1MDAxNWi+XHRkXHUwMDE5Nlxy61x1MDAwN1x1MDAwM1S83zJcIkCdi7hup2ErXYvr5bhe7Vx1MDAxZNh9h8XWXHUwMDA08Z6/57DJx+lAslx1MDAxZfDRPIFPUeVcdTAwMGWoNEqX/jZWXHUwMDA0XHUwMDFmozjgXHUwMDE1XHUwMDAyNJt1qaxcdTAwMWG4dybb+DGNzs10x6RcdTAwMDJcdTAwMGX70Fx0K1x1MDAxZIByomBQWlx1MDAxYbbfzjdcdTAwMTOgXCI3OKYkbKfrjVotTnnq91x1MDAxYXE97Z/izlxcrnrqn0XhgGHhe8rv67dcdTAwMTFN/429Rj/7aymjUOef7t///DH06GJk+89cdTAwMDCms697kf89tXWjnMXvM27KIbFHUZOnXHUwMDE4RuvWxbVtLrDagNOs4ZC07a2yoaCAxYzPXHUwMDBlk1VujilcdTAwMDbWiXwlQYJcdTAwMTWU0EbYIcZcclx1MDAwMmdRKKOUYjpKUplr7Vo3zSjik6cptc3cuqGRuVr07K3b6LC3z05cdTAwMTBJr0qN1Fxm9yyp1rU3NkDtbaAlXHUwMDBiqI2mgXufyLrFr5Tcfp2EXHUwMDE3sqbOpfy6cX1T2y1cdTAwMThcdTAwMTMqjdbDylk2pzJcdTAwMTNcdTAwMTC5QVx0ckBsXG6kNlxmwsFB/Vxc5q1cYtudvVx1MDAwM6ielX1j/qv+zT9cZlx1MDAxY1+RXHUwMDFmunNTWLjRXG5+cS1cdTAwMWNcdTAwMDb8XHUwMDAwlE/bO3anqle+oXZcdTAwMDFcIu9k22dcdTAwMTVcbphfXFxcdTAwMDYgXHUwMDAzXHUwMDAwa1x1MDAwNFlhnTFmSPaFrTGjwLJcdTAwMWRhU0yoXHUwMDA36lx1MDAwYmBcdTAwMWPfhbR2mqas2Vs4ma+tzt7CjU5cdTAwMDHkrIlcYvyQhEGwguWZyTXN5Fx1MDAxNJXivVx1MDAwNoxiU0PqoVx1MDAxNq7S/lK3Z+dX71x1MDAwZk/v1j7U6u06bSbDx6S0XHUwMDE3KlqTIYRcXMCQXHUwMDEznlx1MDAxNiSw3tFOkFwio39u+1ZcYmz/WVx1MDAxOcD0lPatsEQkXHUwMDBiXHUwMDFiUFhcdTAwMWQ7R9O0nH7Q5eZXiCv66HhXvq9cdTAwMWRUV6vh6oybokfVTlx1MDAxZtRcdTAwMTWtKFx1MDAxMFqwW1eCXHUwMDAzXHUwMDA2tH1cdTAwMDVcIoOBN3uoOTz1TnSOKSeX3XSWcspcdTAwMDbbNV6CXHThi+tTXHUwMDE4r4ennJrutLpcdTAwMGLXglx1MDAxZn1cZrXTt5eVzcPV5Tw2XHUwMDFmUVx1MDAwNtXeWT825Vx1MDAwNKZnazfllNnWXHUwMDFmKaeDtFx1MDAxNTd//7tcdTAwMWRV/a3/sdT9I1xign9cbjJPpudb5p15XHUwMDFhM8KRXHUwMDE0XHUwMDFm2SFBrjBcdTAwMDcl2WFcdTAwMWLjYPJcdTAwMTTUaJ26mFx1MDAxZFx1MDAxMiyZXHUwMDE1WuGYOuznkC1bX6tcdTAwMTmQXHQ0XGLtrFx1MDAwMsFm4HF9PHPukGDlyVx1MDAwMac09DhcdTAwMDXzyFx1MDAxNol5lpmc8vX6p2lcdTAwMWZcdTAwMDKhXG6TXHUwMDE44PhRXHUwMDAyu93Jm9lHi5xcdTAwMDWlh1xmkNhcdTAwMDc64dtshctJ447ClypcdTAwMDDBXHUwMDFhS1x1MDAxOO8rXHUwMDFm14Y5Z26AsnyE1e5Z24fmWapxXHUwMDFjc1x1MDAxMsCsuFGoXHJdcXMya1SGis7Vvca2XHUwMDBm7ey+Wa2JVbprn8qtV4d3ur3eXFx4cehcdTAwMDKWKOTDXGaSyvSFvVx1MDAwMlx1MDAwMo5cIrVi1iiL+nG0mIk2lKCcT2nPTFx1MDAxYY4y0IpcdTAwMWM9KID9Jd4m7/UpXlx1MDAxMmhcclqj5Vx1MDAxNFx1MDAxNNy6OUluV6rHuGJaXHUwMDFm4NPm2+al2lt4XG6awHJ879dcdTAwMDEqYuXWl1xclzaQzvpYXHUwMDE1XGa7apxjXHUwMDAz32RcdTAwMWN00oBcdTAwMTb6SVx1MDAxNqp8d1x1MDAwNL9cdTAwMDKo+XIwt6KyP0WCWrEnzFx1MDAxNSrGUVx1MDAxMOyO2Wx93Ph8udZ6Xd6/Wf2y594+qzZcdTAwMWNPQFx1MDAxOVxiTSiZX9pIYUxfr51cZlhxSak5ZNLWWFPsXHUwMDA1J1g3PkJcdTAwMWOCXHUwMDFltrJxsCWHdaomO11otKj5XHUwMDEx31xi/3QtOe+YY0ure1tLx51cdTAwMDaYRWjF6Vx1MDAxZtJIXHUwMDAyXHUwMDE3XHUwMDE3cUYsYpJoWcTRXHUwMDE0XHUwMDFkOKNcdTAwMWb5nFx1MDAxNys/iMLAUZNwQCT9anW/YLmHwlx1MDAxY3WRr4Zw0GVcdTAwMTSCUXOjMFx1MDAwNYTWknFIXHUwMDFjn1x0NaQlnny/vkFnObwgXHUwMDA0kZuke1x1MDAwN8tcdTAwMDGqtbNskJ9cdTAwMGJcdTAwMDMnrNKMdlxiS/mKiOis7idJfPdgncDBMlxyYGBJcEDCqoBcdTAwMDW6+1FcdTAwMDadskwzOpG/1FuI9kVvK3iiSFGuWNpcdTAwMWSTXHUwMDBifMOmlpLHbCzan7xcdTAwMGVdXGJh/1x1MDAxOVx1MDAwMG/2dS/yv1x1MDAxZpamyivQgcZ/we53mj7C6t715/2tVXVU0lx1MDAxOFx1MDAxZlbLpdKnXHUwMDBm5WdVXCJ6glx1MDAxY67TQlx1MDAxMlx1MDAwNz2ChYjvWO2VXCJWeKmiXGZHTSSketx69lx1MDAxMWkq3SnHOVx1MDAxZYhfpVx1MDAwNjCsV3hsnspJpoPLv0fj/yxN5ZdcdTAwMDbr2b07p7Bx3lx1MDAxNb81R1xu7H2pxThWbOxcXNXWV1x1MDAxMnXSbJ7WTva3dr/S6eYzh8hqrELnXHUwMDE4XHUwMDE4hUNk0Fx1MDAxYt/Q2Mdcbun7z7T2K2G1yq9cdTAwMWV6tlx1MDAxMiZTVEF+1c78XFw4oWK/RFOg8FeM/FBcdTAwMDdcdTAwMDXSXHUwMDE1UdGvsFx1MDAxNEZbOTlcdTAwMTe/bJ6+XHKbXHUwMDA3d28+XHUwMDFjmbXD+MvtdvV6ti9cdTAwMGKYh4cygpTtLDtcdTAwMDOHorebgJ1WYDlQXHUwMDE2llU2MiFcdTAwMTe6yFxiyFx1MDAxNlx1MDAwM5Sc/zrsUeyVOt/HOi9cdTAwMWZcIlxugauclkhcdTAwMDCT1//e1Y/dUSk+Xr9z9crhyWbra7PZWHhcdTAwMWZCXHUwMDAxaybDN0vsRYj6gFx1MDAwYqyspOr0wVwi+ddQPLtcdTAwMGbRvulVXHUwMDFhN7uVVyN9XGLz+bErmX/5kIl8yIhcdTAwMTfLsbBHSTDFi+VWReVqy1RcZiRXeq/0Pv566q6KSlx1MDAxZU/jQ8Yy0SdjgONKwzwzXHUwMDFj6vfmaqSSbNlccvnXfKAzrn9cXFx1MDAwYuVBWHBKwdHa8zpcdTAwMTBcdTAwMDdi7rVySYWoVWA0+ZU3XHUwMDEzg/a2vXF516Aj65JqUrYnXHUwMDFi+u7t60V3IJqDc0SD/MDJZ+dUXHUwMDFmbFn5MJJASHRkzLOX6Tg0XHUwMDE1pK1+klo5+1x1MDAwZmaanlx1MDAwMoS//Mf3XGZDZ06Xw2bzIOVcdTAwMTntmlx1MDAwN35Wcfl+WrLrLF/F0fXasFx1MDAxN791Pv5bO7z2XGaKOrbm24tv/1x1MDAwM0RNWtcifQ== + + + + widget.render_line(y=0)widget.render_line(y=1)widget.render_line(y=2)Strip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])Line API WidgetStrip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...]) diff --git a/docs/images/segment.excalidraw.svg b/docs/images/segment.excalidraw.svg new file mode 100644 index 000000000..a1a521b85 --- /dev/null +++ b/docs/images/segment.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1aW1PbRlx1MDAxOH3nVzDuSztcdTAwMTM2e790ptOhJFx1MDAxNEIuUEJoaTuMsNa2gixcdTAwMTlJ5pJO/nu/XHUwMDE1RpIl7CBjUqd+XHUwMDAwe3el/Xb3nPNdpH/W1tc72c3Idn5cXO/Y665cdTAwMTdcdTAwMDZ+4l11nrn2S5ukQVx1MDAxY0FcdTAwMTfNf6fxOOnmI1x1MDAwN1k2Sn98/nzoJec2XHUwMDFihV7XossgXHUwMDFke2Gajf0gRt14+DzI7DD92f1961xy7U+jeOhnXHQqJ9mwfpDFye1cXDa0Q1x1MDAxYmUp3P1P+L2+/k/+t2JdYruZXHUwMDE39UObX5B3lVx1MDAwNirJ661v4yg3lkhFhGRcdTAwMWHLYkSQvoD5MutDd1x1MDAwZmy2ZY9r6vAjuvlhK+7ffPQvtlx1MDAwZcJNSo7CP8ppe0FcdTAwMThcdTAwMWVmN+HtVnjdwTipXHUwMDE4lWZJfG6PXHUwMDAzP1x1MDAxYrjZa+3Fdb6XXHUwMDBlwICiO4nH/UFkU7dcdTAwMDO4aI1HXjfIbtyNcNl6u1xy1XHX7pBcZkZSca41Zsatueh11zNKXHUwMDExx0ZLyZRcdTAwMTCMypphW3FcYmdcdTAwMDGGfYfzT2nZmdc974N5kV+MyVx1MDAxMi9KR15cdTAwMDInVo67miyZXHUwMDBiiqQglCqMXHKunszAXHUwMDA2/UHmjosgho1SynDMsJCiNMbmp0JcZnN2XHUwMDEyo4tcdTAwMWVnwmjXz1x1MDAxMfJ3ddtcIn+ybXeQKUHDJi2fy8W48S/rYKtcdTAwMDKugoN3e0dXxmNDtn/iXHUwMDFk7uyeblx1MDAxZqTvVLHgKXR6SVx1MDAxMl91ip7Pk2+loeOR791cIlx1MDAwZVx1MDAwMCk4XHUwMDExWlFDTdFcdTAwMWZcdTAwMDbROXRG4zAs2+LueVx00rXKStqxg1a2scZcdTAwMGVcdTAwMDBcdTAwMGLRilx1MDAxOf5gclxcnrw7vd5S9rV93dvjsYjt/lx0WYxcdTAwMWN0XHUwMDE2OdJcdTAwMTgk4n5ukFx1MDAwNblhkFwiXFxzXHUwMDAw1TQtXHUwMDE4QVhSMZNcclpSa7qLs4FiiaQhXHUwMDFjJuHGfVSTXHUwMDBlgiFtpm2744HEmlxuzivn80Q8mIdUXHUwMDAxu8SWhdTMXmf3gnQmRlx1MDAwNTVcZnSLPlxcwI9Aen57b7btx4R9XHUwMDE0Nt7ZXHUwMDBlg8GSXHUwMDA1fOlcdTAwMThlRFwirakmtFx1MDAwNlH+dFpNKtJbwJHyOlxmXHKmXHUwMDA0XHUwMDBiptqgcFxuXHUwMDFmrWT3evx7cPkq+nBp9/ax2Lu+Or5cdTAwMWPsLUl2OWfaXHUwMDE40lx1MDAwMswlauIoO1xmPtmc1FOt294wXGLzoyqac5SDgX91dmxcdTAwMTjGz9aP4yT0/+pUjyq1MHvtdu66zTDoO0Z0QtubpkpcdTAwMTZA8FR0Z/Go7O2CXHUwMDFkXHUwMDFl3C7Z9evriZOgXHUwMDFmRF74fpZNi3tcdTAwMTbGVL31jrWGXHUwMDEwIK3hXHUwMDBmZ+3u3ubbbSrVxYuD991cdTAwMWXTn3x/J5jB2lx1MDAxYfv+K79cIihGVIBCmqZjwVxiT7XWucsk73XVIzxcdTAwMGJhSNx5ldaeRTpBxZqV6/m/XHUwMDA1WMYwgkvFelwit0VmM4AoyjGRpEVwdfM2ftU/O9u/XHUwMDEx4uJ0g1x1MDAxZI9e71x1MDAxZmyuuuNcdTAwMTJMI6yVXHUwMDEyhlPnKNg0XHUwMDExuECSc8HInCjr0X5Mmyb4m35MSaG5Ulx1MDAxNU16Sj/2q/fJo3tcdTAwMDO6uSuOduTxsWTnLy+Wlj5cdTAwMTjNOG2B7sf5sVx1MDAxYz3fn8Wh/9P7ZGx/WFx1MDAwNT/WsGmxuLNidZ3AnEHKXHUwMDAwaWhcdTAwMTmafonAL0/UwcvRfnK+8bt9dbQpe1x1MDAxYofh6apcdTAwMTOYUYYoJNzwMVxuXHUwMDEy93K57nouOVx1MDAwMvpSXHUwMDE3PHHYjbp3XVwii+k9Low2XFyXkka6QK6N61rNaFRcdTAwMTBcdTAwMTBF9lQsJlNsdFx1MDAwNvZcdTAwMTNrsyDqo2kyVChcXIH616DwtEGL8Vx1MDAxN8/kL/gjXHR7rMpcdTAwMDFfou9V/8Ppm1x1MDAxNyPpXHUwMDFmvtl68UZEoThcdTAwMWOfrDp9hYJA0HBiZO6AWZ2+XG5RIDV0XHUwMDAwezWresVl87dCyTn8hWReSY1L2H+zTphcdTAwMTCBK1x1MDAxOfRXo2+aY2mV+Htr0VxcXHUwMDAy327vfUlkpVxmWvfAWlx1MDAwYlx1MDAwMVx1MDAxOU5cdTAwMGJcdTAwMGY8X69XlMJMXHUwMDBiJFx1MDAwNGR04Fx1MDAwZaRcdTAwMDF/PFx1MDAxZENzSZCmmFNtnthcdTAwMDVcdTAwMWKkQElcXKFEaqOwvqc8pDRSICXmLlx1MDAxY6hcdTAwMThzV7yXcFx1MDAwM6ZcdTAwMWVXu6eTlsVqlos71jTzkuyXIPJcdTAwMDHW04ZNnlHtPiDQy7ncXHUwMDFkOys3MMJcXFx1MDAxMVc9g3CSUWp4+WjG7Y03cqtFkuWlv8aqbeR/2Zr5XHUwMDA1z4o1YFxmwVx1MDAwNsPeMEyoNIRy1TCGMMjn8pJGw5rQS7OteDhcZjLY7v04iLL6tub7t+m4PrBeQ0BgNdW+uiiM3Fx1MDAxZKc1vfy2XpIm/1F8//vZvaNnYtl9Nlx1MDAxYTAub7dW/d9azqiaWclcdTAwMDYh4EJcYqVcdTAwMWUuZ/P914rKmWRcdTAwMWNcdTAwMTmqMVMu6IDMoaFmsFx1MDAwNdJhzEUkrGbX8tRcZuJcIsyNUkZcdTAwMGLIb4SsJP6lnCmEXHUwMDFkXHUwMDE1INOrXHUwMDE5M1EzJjXRXHUwMDFjyzZFg2XL2eLZ/lx1MDAwM+VsfuBblzNcdTAwMDaUYiBoXHUwMDEwVzqK8cq4W1x1MDAwNVx1MDAxMYgzdr+CPEjP5tfBpvWMXG7KNVx1MDAwMVx1MDAwNitOsKaGNawhXHUwMDAykVx1MDAxOer6LenZxmw45911JLdcdTAwMTS0WVx0lmGi3lokWFxmpuK0zZO5M9zt9a8hXHUwMDFhXHUwMDFkXHUwMDFmXnw8+qN38mv8epFcdTAwMWH/XCJitth7XHUwMDE1Tq5cdTAwMDTPdUxgXHUwMDAzwVlZpHA3oJQh8KJGXHUwMDEy6TA/O7tcdTAwMDLn7/H5YvZdr9drqph6UFWEQOKHtWStdGrxvOqJS/dSV6pQXyuvWqWMasFcXIpSQurNRfRcdTAwMDFbXHUwMDBiQCUtkqn5p7ySj+S4XHUwMDExiCqmlVCUS1x1MDAxMMKSkTldhUBaQSxuXHUwMDA0eHZiKq/GLI2wXHUwMDEwP3NcZiqtJOdcdTAwMWFCoEq4Vz6ZI65mg4VDPDZcdTAwMTJX9GzygFx1MDAwZbSVske+XHUwMDAw9aioQ2JcdTAwMTeutnlFqW3UMd9cdTAwMWJMuXnKMVx1MDAwM99GKTbCbUwz5lBIuUhAaYWpXHUwMDE0RuNcdTAwMDVjj/lv/9ViXHUwMDBmLMDlau5cdTAwMWVwSaFpwyhcdTAwMDKBMNPgrImGMFx1MDAxMoY0n5h+SyHIbGS7T1x1MDAwM9OzXCKQtclcdTAwMDRcdTAwMWRvNDrMXHUwMDAwdMVxXHUwMDAwylx1MDAwM39cIuDlKjuXgb365X76OVx1MDAwNq5N9tNcdJLNmfB57fO/MTl+mCJ9 + + + + "Hello, World"Style(bold=True)greeting.textgreeting.stylegreeting diff --git a/mkdocs.yml b/mkdocs.yml index 1748de127..bbb962580 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -168,6 +168,7 @@ nav: - "api/reactive.md" - "api/screen.md" - "api/static.md" + - "api/strip.md" - "api/text_log.md" - "api/timer.md" - "api/tree.md" diff --git a/poetry.lock b/poetry.lock index 7cd75899a..c1ee65e43 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,3 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. - [[package]] name = "aiohttp" version = "3.8.3" @@ -7,7 +5,1045 @@ description = "Async http client/server framework (asyncio)" category = "main" optional = false python-versions = ">=3.6" -files = [ + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""} +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "cchardet"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "anyio" +version = "3.6.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16,<0.22)"] + +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} + +[[package]] +name = "asynctest" +version = "0.13.0" +description = "Enhance the standard unittest package with features for testing asyncio libraries" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "23.1.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cached-property" +version = "1.5.2" +description = "A decorator for caching properties in classes." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" + +[[package]] +name = "colored" +version = "1.4.4" +description = "Simple library for color and formatting to terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "coverage" +version = "7.1.0" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "distlib" +version = "0.3.6" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.9.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "frozenlist" +version = "1.3.3" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "gitdb" +version = "4.0.10" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.30" +description = "GitPython is a python library used to interact with Git repositories" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} + +[[package]] +name = "griffe" +version = "0.25.4" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cached-property = {version = "*", markers = "python_version < \"3.8\""} +colorama = ">=0.4" + +[package.extras] +async = ["aiofiles (>=0.7,<1.0)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "httpcore" +version = "0.16.3" +description = "A minimal low-level HTTP client." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.23.3" +description = "The next generation HTTP client." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.17.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "identify" +version = "2.5.17" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "4.13.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +perf = ["ipython"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "jinja2" +version = "3.0.3" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markdown" +version = "3.3.7" +description = "Python implementation of Markdown." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "2.1.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +mdurl = ">=0.1,<1.0" +typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] +code-style = ["pre-commit (==2.6)"] +compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] +linkify = ["linkify-it-py (>=1.0,<2.0)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mkdocs" +version = "1.4.2" +description = "Project documentation with Markdown." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.2.1,<3.4" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "0.4.1" +description = "Automatically link across pages in MkDocs." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Markdown = ">=3.3" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-exclude" +version = "1.0.2" +description = "A mkdocs plugin that lets you exclude files or trees." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +mkdocs = "*" + +[[package]] +name = "mkdocs-material" +version = "8.5.11" +description = "Documentation that simply works" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +jinja2 = ">=3.0.2" +markdown = ">=3.2" +mkdocs = ">=1.4.0" +mkdocs-material-extensions = ">=1.1" +pygments = ">=2.12" +pymdown-extensions = ">=9.4" +requests = ">=2.26" + +[[package]] +name = "mkdocs-material-extensions" +version = "1.1.1" +description = "Extension pack for Python Markdown and MkDocs Material." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mkdocs-rss-plugin" +version = "1.5.0" +description = "MkDocs plugin which generates a static RSS feed using git log and page.meta." +category = "dev" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +GitPython = ">=3.1,<3.2" +mkdocs = ">=1.1,<2" +pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""} +tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""} + +[package.extras] +dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"] +doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"] + +[[package]] +name = "mkdocstrings" +version = "0.20.0" +description = "Automatic documentation from sources, for MkDocs." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.2" +mkdocs-autorefs = ">=0.3.1" +mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "0.8.3" +description = "A Python handler for mkdocstrings." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +griffe = ">=0.24" +mkdocstrings = ">=0.19" + +[[package]] +name = "msgpack" +version = "1.0.4" +description = "MessagePack serializer" +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mypy" +version = "0.990" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nanoid" +version = "2.0.0" +description = "A tiny, secure, URL-friendly, unique string ID generator for Python" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.0" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "pathspec" +version = "0.11.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "platformdirs" +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pygments" +version = "2.14.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pymdown-extensions" +version = "9.9.2" +description = "Extension pack for Python Markdown." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +markdown = ">=3.2" + +[[package]] +name = "pytest" +version = "7.2.1" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-aiohttp" +version = "1.0.4" +description = "Pytest plugin for aiohttp support" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +aiohttp = ">=3.8.1" +pytest = ">=6.1.0" +pytest-asyncio = ">=0.17.2" + +[package.extras] +testing = ["coverage (==6.2)", "mypy (==0.931)"] + +[[package]] +name = "pytest-asyncio" +version = "0.20.3" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2022.7.1" +description = "World timezone definitions, modern and historical" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "requests" +version = "2.28.2" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "13.3.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.7.0" + +[package.dependencies] +markdown-it-py = ">=2.1.0,<3.0.0" +pygments = ">=2.14.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "setuptools" +version = "67.1.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "syrupy" +version = "3.0.6" +description = "Pytest Snapshot Test Utility" +category = "dev" +optional = false +python-versions = ">=3.7,<4" + +[package.dependencies] +colored = ">=1.3.92,<2.0.0" +pytest = ">=5.1.0,<8.0.0" + +[[package]] +name = "time-machine" +version = "2.9.0" +description = "Travel through time in your tests." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +python-dateutil = "*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typed-ast" +version = "1.5.4" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tzdata" +version = "2022.7" +description = "Provider of IANA time zone data" +category = "dev" +optional = false +python-versions = ">=2" + +[[package]] +name = "urllib3" +version = "1.26.14" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.17.1" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} +platformdirs = ">=2.4,<3" + +[package.extras] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "watchdog" +version = "2.2.1" +description = "Filesystem events monitoring" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "yarl" +version = "1.8.2" +description = "Yet another URL library" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[[package]] +name = "zipp" +version = "3.12.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[extras] +dev = ["aiohttp", "click", "msgpack"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "8b9c57d32f9db7d59bacc1e254e46bc5ae523e9e831494c205caf1b5fe7982e4" + +[metadata.files] +aiohttp = [ {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"}, {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"}, {file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"}, @@ -96,250 +1132,81 @@ files = [ {file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"}, {file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"}, ] - -[package.dependencies] -aiosignal = ">=1.1.2" -async-timeout = ">=4.0.0a3,<5.0" -asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""} -attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns", "cchardet"] - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +aiosignal = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, ] - -[package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "anyio" -version = "3.6.2" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "dev" -optional = false -python-versions = ">=3.6.2" -files = [ +anyio = [ {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, ] - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] - -[[package]] -name = "async-timeout" -version = "4.0.2" -description = "Timeout context manager for asyncio programs" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] - -[package.dependencies] -typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} - -[[package]] -name = "asynctest" -version = "0.13.0" -description = "Enhance the standard unittest package with features for testing asyncio libraries" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ +asynctest = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, ] - -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +attrs = [ {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, ] - -[package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] - -[[package]] -name = "black" -version = "22.8.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.6.2" -files = [ - {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, - {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, - {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, - {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, - {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, - {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, - {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, - {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, - {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, - {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, - {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, - {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, - {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, - {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, - {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, - {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, - {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, - {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, - {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, +black = [ + {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, + {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, + {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, + {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, + {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, + {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, + {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, + {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, + {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, + {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, + {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, + {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, + {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, + {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, ] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -category = "dev" -optional = false -python-versions = "*" -files = [ +cached-property = [ {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, ] - -[[package]] -name = "certifi" -version = "2022.12.7" -description = "Python package for providing Mozilla's CA Bundle." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +certifi = [ {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] - -[[package]] -name = "cfgv" -version = "3.3.1" -description = "Validate configuration and produce human readable error messages." -category = "dev" -optional = false -python-versions = ">=3.6.1" -files = [ +cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] - -[[package]] -name = "charset-normalizer" -version = "2.1.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = ">=3.6.0" -files = [ +charset-normalizer = [ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] - -[package.extras] -unicode-backport = ["unicodedata2"] - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ +colorama = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] - -[[package]] -name = "colored" -version = "1.4.4" -description = "Simple library for color and formatting to terminal" -category = "dev" -optional = false -python-versions = "*" -files = [ +colored = [ {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"}, ] - -[[package]] -name = "coverage" -version = "7.1.0" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +coverage = [ {file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"}, {file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"}, {file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"}, @@ -392,61 +1259,19 @@ files = [ {file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"}, {file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"}, ] - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "distlib" -version = "0.3.6" -description = "Distribution utilities" -category = "dev" -optional = false -python-versions = "*" -files = [ +distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] - -[[package]] -name = "exceptiongroup" -version = "1.1.0" -description = "Backport of PEP 654 (exception groups)" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +exceptiongroup = [ {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, ] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "filelock" -version = "3.9.0" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +filelock = [ {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, ] - -[package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "frozenlist" -version = "1.3.3" -description = "A list-like structure which implements collections.abc.MutableSequence" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +frozenlist = [ {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, @@ -522,266 +1347,63 @@ files = [ {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, ] - -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -category = "main" -optional = false -python-versions = "*" -files = [ +ghp-import = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "gitdb" -version = "4.0.10" -description = "Git Object Database" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +gitdb = [ {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, ] - -[package.dependencies] -smmap = ">=3.0.1,<6" - -[[package]] -name = "gitpython" -version = "3.1.30" -description = "GitPython is a python library used to interact with Git repositories" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +gitpython = [ {file = "GitPython-3.1.30-py3-none-any.whl", hash = "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882"}, {file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"}, ] - -[package.dependencies] -gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} - -[[package]] -name = "griffe" -version = "0.25.4" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +griffe = [ {file = "griffe-0.25.4-py3-none-any.whl", hash = "sha256:919f935a358b31074d16e324e26b041883c60a8cf10504655e394afc3a7caad8"}, {file = "griffe-0.25.4.tar.gz", hash = "sha256:f190edf8ef58d43c856d2d6761ec324a043ff60deb8c14359263571e8b91fe68"}, ] - -[package.dependencies] -cached-property = {version = "*", markers = "python_version < \"3.8\""} -colorama = ">=0.4" - -[package.extras] -async = ["aiofiles (>=0.7,<1.0)"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +h11 = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "httpcore" -version = "0.16.3" -description = "A minimal low-level HTTP client." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +httpcore = [ {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, ] - -[package.dependencies] -anyio = ">=3.0,<5.0" -certifi = "*" -h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" - -[package.extras] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[[package]] -name = "httpx" -version = "0.23.3" -description = "The next generation HTTP client." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +httpx = [ {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, ] - -[package.dependencies] -certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[[package]] -name = "identify" -version = "2.5.15" -description = "File identification library for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "identify-2.5.15-py2.py3-none-any.whl", hash = "sha256:1f4b36c5f50f3f950864b2a047308743f064eaa6f6645da5e5c780d1c7125487"}, - {file = "identify-2.5.15.tar.gz", hash = "sha256:c22aa206f47cc40486ecf585d27ad5f40adbfc494a3fa41dc3ed0499a23b123f"}, +identify = [ + {file = "identify-2.5.17-py2.py3-none-any.whl", hash = "sha256:7d526dd1283555aafcc91539acc061d8f6f59adb0a7bba462735b0a318bff7ed"}, + {file = "identify-2.5.17.tar.gz", hash = "sha256:93cc61a861052de9d4c541a7acb7e3dcc9c11b398a2144f6e52ae5285f5f4f06"}, ] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ +idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] - -[[package]] -name = "importlib-metadata" -version = "4.13.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +importlib-metadata = [ {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, ] - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +iniconfig = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] - -[[package]] -name = "jinja2" -version = "3.0.3" -description = "A very fast and expressive template engine." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +jinja2 = [ {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, ] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markdown" -version = "3.3.7" -description = "Python implementation of Markdown." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +markdown = [ {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, ] - -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - -[package.extras] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markdown-it-py" -version = "2.1.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +markdown-it-py = [ {file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"}, {file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"}, ] - -[package.dependencies] -mdurl = ">=0.1,<1.0" -typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] -code-style = ["pre-commit (==2.6)"] -compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] -linkify = ["linkify-it-py (>=1.0,<2.0)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "2.1.2" -description = "Safely add untrusted strings to HTML/XML markup." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +markupsafe = [ {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, @@ -833,196 +1455,46 @@ files = [ {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, ] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +mdurl = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +mergedeep = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] - -[[package]] -name = "mkdocs" -version = "1.4.2" -description = "Project documentation with Markdown." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +mkdocs = [ {file = "mkdocs-1.4.2-py3-none-any.whl", hash = "sha256:c8856a832c1e56702577023cd64cc5f84948280c1c0fcc6af4cd39006ea6aa8c"}, {file = "mkdocs-1.4.2.tar.gz", hash = "sha256:8947af423a6d0facf41ea1195b8e1e8c85ad94ac95ae307fe11232e0424b11c5"}, ] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} -jinja2 = ">=2.11.1" -markdown = ">=3.2.1,<3.4" -mergedeep = ">=1.3.4" -packaging = ">=20.5" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-autorefs" -version = "0.4.1" -description = "Automatically link across pages in MkDocs." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocs-autorefs = [ {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, ] - -[package.dependencies] -Markdown = ">=3.3" -mkdocs = ">=1.1" - -[[package]] -name = "mkdocs-exclude" -version = "1.0.2" -description = "A mkdocs plugin that lets you exclude files or trees." -category = "main" -optional = false -python-versions = "*" -files = [ +mkdocs-exclude = [ {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"}, ] - -[package.dependencies] -mkdocs = "*" - -[[package]] -name = "mkdocs-material" -version = "8.5.11" -description = "Documentation that simply works" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocs-material = [ {file = "mkdocs_material-8.5.11-py3-none-any.whl", hash = "sha256:c907b4b052240a5778074a30a78f31a1f8ff82d7012356dc26898b97559f082e"}, {file = "mkdocs_material-8.5.11.tar.gz", hash = "sha256:b0ea0513fd8cab323e8a825d6692ea07fa83e917bb5db042e523afecc7064ab7"}, ] - -[package.dependencies] -jinja2 = ">=3.0.2" -markdown = ">=3.2" -mkdocs = ">=1.4.0" -mkdocs-material-extensions = ">=1.1" -pygments = ">=2.12" -pymdown-extensions = ">=9.4" -requests = ">=2.26" - -[[package]] -name = "mkdocs-material-extensions" -version = "1.1.1" -description = "Extension pack for Python Markdown and MkDocs Material." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocs-material-extensions = [ {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, ] - -[[package]] -name = "mkdocs-rss-plugin" -version = "1.5.0" -description = "MkDocs plugin which generates a static RSS feed using git log and page.meta." -category = "dev" -optional = false -python-versions = ">=3.7, <4" -files = [ +mkdocs-rss-plugin = [ {file = "mkdocs-rss-plugin-1.5.0.tar.gz", hash = "sha256:4178b3830dcbad9b53b12459e315b1aad6b37d1e7e5c56c686866a10f99878a4"}, {file = "mkdocs_rss_plugin-1.5.0-py2.py3-none-any.whl", hash = "sha256:2ab14c20bf6b7983acbe50181e7e4a0778731d9c2d5c38107ca7047a7abd2165"}, ] - -[package.dependencies] -GitPython = ">=3.1,<3.2" -mkdocs = ">=1.1,<2" -pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""} -tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""} - -[package.extras] -dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"] -doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"] - -[[package]] -name = "mkdocstrings" -version = "0.20.0" -description = "Automatic documentation from sources, for MkDocs." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocstrings = [ {file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"}, {file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"}, ] - -[package.dependencies] -Jinja2 = ">=2.11.1" -Markdown = ">=3.3" -MarkupSafe = ">=1.1" -mkdocs = ">=1.2" -mkdocs-autorefs = ">=0.3.1" -mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} -pymdown-extensions = ">=6.3" - -[package.extras] -crystal = ["mkdocstrings-crystal (>=0.3.4)"] -python = ["mkdocstrings-python (>=0.5.2)"] -python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] - -[[package]] -name = "mkdocstrings-python" -version = "0.8.3" -description = "A Python handler for mkdocstrings." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocstrings-python = [ {file = "mkdocstrings-python-0.8.3.tar.gz", hash = "sha256:9ae473f6dc599339b09eee17e4d2b05d6ac0ec29860f3fc9b7512d940fc61adf"}, {file = "mkdocstrings_python-0.8.3-py3-none-any.whl", hash = "sha256:4e6e1cd6f37a785de0946ced6eb846eb2f5d891ac1cc2c7b832943d3529087a7"}, ] - -[package.dependencies] -griffe = ">=0.24" -mkdocstrings = ">=0.19" - -[[package]] -name = "msgpack" -version = "1.0.4" -description = "MessagePack serializer" -category = "main" -optional = true -python-versions = "*" -files = [ +msgpack = [ {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"}, {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88"}, {file = "msgpack-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467"}, @@ -1076,15 +1548,7 @@ files = [ {file = "msgpack-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce"}, {file = "msgpack-1.0.4.tar.gz", hash = "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f"}, ] - -[[package]] -name = "multidict" -version = "6.0.4" -description = "multidict implementation" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +multidict = [ {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, @@ -1160,15 +1624,7 @@ files = [ {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, ] - -[[package]] -name = "mypy" -version = "0.990" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mypy = [ {file = "mypy-0.990-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aaf1be63e0207d7d17be942dcf9a6b641745581fe6c64df9a38deb562a7dbafa"}, {file = "mypy-0.990-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d555aa7f44cecb7ea3c0ac69d58b1a5afb92caa017285a8e9c4efbf0518b61b4"}, {file = "mypy-0.990-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f694d6d09a460b117dccb6857dda269188e3437c880d7b60fa0014fa872d1e9"}, @@ -1200,290 +1656,71 @@ files = [ {file = "mypy-0.990-py3-none-any.whl", hash = "sha256:8f1940325a8ed460ba03d19ab83742260fa9534804c317224e5d4e5aa588e2d6"}, {file = "mypy-0.990.tar.gz", hash = "sha256:72382cb609142dba3f04140d016c94b4092bc7b4d98ca718740dc989e5271b8d"}, ] - -[package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" -optional = false -python-versions = "*" -files = [ +mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] - -[[package]] -name = "nanoid" -version = "2.0.0" -description = "A tiny, secure, URL-friendly, unique string ID generator for Python" -category = "main" -optional = false -python-versions = "*" -files = [ +nanoid = [ {file = "nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb"}, {file = "nanoid-2.0.0.tar.gz", hash = "sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68"}, ] - -[[package]] -name = "nodeenv" -version = "1.7.0" -description = "Node.js virtual environment builder" -category = "dev" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ +nodeenv = [ {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, ] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "packaging" -version = "23.0" -description = "Core utilities for Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +packaging = [ {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] - -[[package]] -name = "pathspec" -version = "0.11.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pathspec = [ {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, ] - -[[package]] -name = "platformdirs" -version = "2.6.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +platformdirs = [ {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, ] - -[package.dependencies] -typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "2.21.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pre-commit = [ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, ] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pygments" -version = "2.14.0" -description = "Pygments is a syntax highlighting package written in Python." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +pygments = [ {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, ] - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pymdown-extensions" -version = "9.9.2" -description = "Extension pack for Python Markdown." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pymdown-extensions = [ {file = "pymdown_extensions-9.9.2-py3-none-any.whl", hash = "sha256:c3d804eb4a42b85bafb5f36436342a5ad38df03878bb24db8855a4aa8b08b765"}, {file = "pymdown_extensions-9.9.2.tar.gz", hash = "sha256:ebb33069bafcb64d5f5988043331d4ea4929325dc678a6bcf247ddfcf96499f8"}, ] - -[package.dependencies] -markdown = ">=3.2" - -[[package]] -name = "pytest" -version = "7.2.1" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pytest = [ {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, ] - -[package.dependencies] -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] - -[[package]] -name = "pytest-aiohttp" -version = "1.0.4" -description = "Pytest plugin for aiohttp support" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pytest-aiohttp = [ {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"}, {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"}, ] - -[package.dependencies] -aiohttp = ">=3.8.1" -pytest = ">=6.1.0" -pytest-asyncio = ">=0.17.2" - -[package.extras] -testing = ["coverage (==6.2)", "mypy (==0.931)"] - -[[package]] -name = "pytest-asyncio" -version = "0.20.3" -description = "Pytest support for asyncio" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pytest-asyncio = [ {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, ] - -[package.dependencies] -pytest = ">=6.1.0" -typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] - -[[package]] -name = "pytest-cov" -version = "2.12.1" -description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ +pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] - -[package.dependencies] -coverage = ">=5.2.1" -pytest = ">=4.6" -toml = "*" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ +python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2022.7.1" -description = "World timezone definitions, modern and historical" -category = "dev" -optional = false -python-versions = "*" -files = [ +pytz = [ {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, ] - -[[package]] -name = "pyyaml" -version = "6.0" -description = "YAML parser and emitter for Python" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -1525,159 +1762,43 @@ files = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +pyyaml-env-tag = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "requests" -version = "2.28.2" -description = "Python HTTP for Humans." -category = "dev" -optional = false -python-versions = ">=3.7, <4" -files = [ +requests = [ {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, ] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "dev" -optional = false -python-versions = "*" -files = [ +rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - -[[package]] -name = "rich" -version = "13.2.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.2.0-py3-none-any.whl", hash = "sha256:7c963f0d03819221e9ac561e1bc866e3f95a02248c1234daa48954e6d381c003"}, - {file = "rich-13.2.0.tar.gz", hash = "sha256:f1a00cdd3eebf999a15d85ec498bfe0b1a77efe9b34f645768a54132ef444ac5"}, +rich = [ + {file = "rich-13.3.1-py3-none-any.whl", hash = "sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9"}, + {file = "rich-13.3.1.tar.gz", hash = "sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f"}, ] - -[package.dependencies] -markdown-it-py = ">=2.1.0,<3.0.0" -pygments = ">=2.6.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] - -[[package]] -name = "setuptools" -version = "66.1.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools-66.1.1-py3-none-any.whl", hash = "sha256:6f590d76b713d5de4e49fe4fbca24474469f53c83632d5d0fd056f7ff7e8112b"}, - {file = "setuptools-66.1.1.tar.gz", hash = "sha256:ac4008d396bc9cd983ea483cb7139c0240a07bbc74ffb6232fceffedc6cf03a8"}, +setuptools = [ + {file = "setuptools-67.1.0-py3-none-any.whl", hash = "sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378"}, + {file = "setuptools-67.1.0.tar.gz", hash = "sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300"}, ] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ +six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] - -[[package]] -name = "smmap" -version = "5.0.0" -description = "A pure Python implementation of a sliding window memory map manager" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +smmap = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] - -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +sniffio = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] - -[[package]] -name = "syrupy" -version = "3.0.6" -description = "Pytest Snapshot Test Utility" -category = "dev" -optional = false -python-versions = ">=3.7,<4" -files = [ +syrupy = [ {file = "syrupy-3.0.6-py3-none-any.whl", hash = "sha256:9c18e22264026b34239bcc87ab7cc8d893eb17236ea7dae634217ea4f22a848d"}, {file = "syrupy-3.0.6.tar.gz", hash = "sha256:583aa5ca691305c27902c3e29a1ce9da50ff9ab5f184c54b1dc124a16e4a6cf4"}, ] - -[package.dependencies] -colored = ">=1.3.92,<2.0.0" -pytest = ">=5.1.0,<8.0.0" - -[[package]] -name = "time-machine" -version = "2.9.0" -description = "Travel through time in your tests." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +time-machine = [ {file = "time-machine-2.9.0.tar.gz", hash = "sha256:60222d43f6e93a926adc36ed37a54bc8e4d0d8d1c4d449096afcfe85086129c2"}, {file = "time_machine-2.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fd72c0b2e7443fff6e4481991742b72c17f73735e5fdd176406ca48df187a5c9"}, {file = "time_machine-2.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5657e0e6077cf15b37f0d8cf78e868113bbb3ecccc60064c40fe52d8166ca8b1"}, @@ -1732,42 +1853,15 @@ files = [ {file = "time_machine-2.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:cc6bf01211b5ea40f633d5502c5aa495b415ebaff66e041820997dae70a508e1"}, {file = "time_machine-2.9.0-cp39-cp39-win_arm64.whl", hash = "sha256:3ce445775fcf7cb4040cfdba4b7c4888e7fd98bbcccfe1dc3fa8a798ed1f1d24"}, ] - -[package.dependencies] -python-dateutil = "*" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ +toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] - -[[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, @@ -1793,78 +1887,23 @@ files = [ {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] - -[[package]] -name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] - -[[package]] -name = "tzdata" -version = "2022.7" -description = "Provider of IANA time zone data" -category = "dev" -optional = false -python-versions = ">=2" -files = [ +tzdata = [ {file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"}, {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"}, ] - -[[package]] -name = "urllib3" -version = "1.26.14" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ +urllib3 = [ {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, ] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "virtualenv" -version = "20.17.1" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +virtualenv = [ {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, ] - -[package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} -platformdirs = ">=2.4,<3" - -[package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] -testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "watchdog" -version = "2.2.1" -description = "Filesystem events monitoring" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +watchdog = [ {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"}, {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5100eae58133355d3ca6c1083a33b81355c4f452afa474c2633bd2fbbba398b3"}, {file = "watchdog-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e618a4863726bc7a3c64f95c218437f3349fb9d909eb9ea3a1ed3b567417c661"}, @@ -1894,18 +1933,7 @@ files = [ {file = "watchdog-2.2.1-py3-none-win_ia64.whl", hash = "sha256:195ab1d9d611a4c1e5311cbf42273bc541e18ea8c32712f2fb703cfc6ff006f9"}, {file = "watchdog-2.2.1.tar.gz", hash = "sha256:cdcc23c9528601a8a293eb4369cbd14f6b4f34f07ae8769421252e9c22718b6f"}, ] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "yarl" -version = "1.8.2" -description = "Yet another URL library" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +yarl = [ {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"}, {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"}, {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"}, @@ -1981,32 +2009,7 @@ files = [ {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"}, {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"}, ] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} - -[[package]] -name = "zipp" -version = "3.11.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, +zipp = [ + {file = "zipp-3.12.0-py3-none-any.whl", hash = "sha256:9eb0a4c5feab9b08871db0d672745b53450d7f26992fd1e4653aa43345e97b86"}, + {file = "zipp-3.12.0.tar.gz", hash = "sha256:73efd63936398aac78fd92b6f4865190119d6c91b531532e798977ea8dd402eb"}, ] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[extras] -dev = ["aiohttp", "click", "msgpack"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.7" -content-hash = "b70dc64a3c9e7a7b765252f5dd1a5de8ed6efacd0695cde32ff983b14ec55ca6" diff --git a/pyproject.toml b/pyproject.toml index 9582f1550..bfd661b74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dev = ["aiohttp", "click", "msgpack"] [tool.poetry.group.dev.dependencies] pytest = "^7.1.3" -black = "^22.3.0,<22.10.0" # macos wheel issue on 22.10 +black = "^23.1.0" mypy = "^0.990" pytest-cov = "^2.12.1" mkdocs = "^1.3.0" diff --git a/src/textual/_animator.py b/src/textual/_animator.py index 330ea2dfd..6ace3dcff 100644 --- a/src/textual/_animator.py +++ b/src/textual/_animator.py @@ -322,7 +322,6 @@ class Animator: ) if animation is None: - if not isinstance(value, (int, float)) and not isinstance( value, Animatable ): diff --git a/src/textual/_parser.py b/src/textual/_parser.py index 0a725c85d..5163a6014 100644 --- a/src/textual/_parser.py +++ b/src/textual/_parser.py @@ -79,7 +79,6 @@ class Parser(Generic[T]): self._awaiting = next(self._gen) def feed(self, data: str) -> Iterable[T]: - if self._eof: raise ParseError("end of file reached") from None if not data: @@ -104,7 +103,6 @@ class Parser(Generic[T]): yield popleft() while pos < data_size or isinstance(self._awaiting, _PeekBuffer): - _awaiting = self._awaiting if isinstance(_awaiting, _Read1): self._awaiting = self._gen.send(data[pos : pos + 1]) diff --git a/src/textual/_sleep.py b/src/textual/_sleep.py index cb632e899..bb931e062 100644 --- a/src/textual/_sleep.py +++ b/src/textual/_sleep.py @@ -38,7 +38,6 @@ class Sleeper(Thread): async def check_sleeps() -> None: - sleeper = Sleeper() sleeper.start() diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index b97a1abd1..7111fca50 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -92,7 +92,6 @@ class XTermParser(Parser[events.Event]): return None def parse(self, on_token: TokenCallback) -> Generator[Awaitable, str, None]: - ESC = "\x1b" read1 = self.read1 sequence_to_key_events = self._sequence_to_key_events @@ -161,7 +160,6 @@ class XTermParser(Parser[events.Event]): # Look ahead through the suspected escape sequence for a match while True: - # If we run into another ESC at this point, then we've failed # to find a match, and should issue everything we've seen within # the suspected sequence as Key events instead. diff --git a/src/textual/cli/previews/colors.py b/src/textual/cli/previews/colors.py index e25c70d7f..3ae0eb669 100644 --- a/src/textual/cli/previews/colors.py +++ b/src/textual/cli/previews/colors.py @@ -30,7 +30,6 @@ class Content(Vertical): class ColorsView(Vertical): def compose(self) -> ComposeResult: - LEVELS = [ "darken-3", "darken-2", @@ -42,7 +41,6 @@ class ColorsView(Vertical): ] for color_name in ColorSystem.COLOR_NAMES: - items: list[Widget] = [Label(f'"{color_name}"')] for level in LEVELS: color = f"{color_name}-{level}" if level else color_name diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 14f1e5404..ff66e9c84 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -434,7 +434,6 @@ class StylesBuilder: process_padding_left = _process_space_partial def _parse_border(self, name: str, tokens: list[Token]) -> BorderValue: - border_type: EdgeType = "solid" border_color = Color(0, 255, 0) diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py index 7bfe1514c..1b98a56f6 100644 --- a/src/textual/css/parse.py +++ b/src/textual/css/parse.py @@ -36,7 +36,6 @@ SELECTOR_MAP: dict[str, tuple[SelectorType, Specificity3]] = { @lru_cache(maxsize=1024) def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]: - if not css_selectors.strip(): return () diff --git a/src/textual/css/query.py b/src/textual/css/query.py index 3f00ee718..7e5c149cc 100644 --- a/src/textual/css/query.py +++ b/src/textual/css/query.py @@ -72,7 +72,6 @@ class DOMQuery(Generic[QueryType]): exclude: str | None = None, parent: DOMQuery | None = None, ) -> None: - self._node = node self._nodes: list[QueryType] | None = None self._filters: list[tuple[SelectorSet, ...]] = ( diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 0bbb66823..5f7ff7235 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -327,7 +327,6 @@ class StylesBase(ABC): # Check we are animating a Scalar or Scalar offset if isinstance(start_value, (Scalar, ScalarOffset)): - # If destination is a number, we can convert that to a scalar if isinstance(value, (int, float)): value = Scalar(value, Unit.CELLS, Unit.CELLS) diff --git a/src/textual/devtools/service.py b/src/textual/devtools/service.py index 6e55456d1..cea646739 100644 --- a/src/textual/devtools/service.py +++ b/src/textual/devtools/service.py @@ -236,7 +236,6 @@ class ClientHandler: message = cast(WSMessage, message) if message.type in (WSMsgType.TEXT, WSMsgType.BINARY): - try: if isinstance(message.data, bytes): message = msgpack.unpackb(message.data) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 2d1b5e1a0..09260be6f 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -92,7 +92,6 @@ class LinuxDriver(Driver): self.console.file.flush() def start_application_mode(self): - loop = asyncio.get_running_loop() def send_size_event(): @@ -123,7 +122,6 @@ class LinuxDriver(Driver): except termios.error: pass else: - newattr[tty.LFLAG] = self._patch_lflag(newattr[tty.LFLAG]) newattr[tty.IFLAG] = self._patch_iflag(newattr[tty.IFLAG]) @@ -208,7 +206,6 @@ class LinuxDriver(Driver): pass # TODO: log def _run_input_thread(self, loop) -> None: - selector = selectors.DefaultSelector() selector.register(self.fileno, selectors.EVENT_READ) diff --git a/src/textual/drivers/windows_driver.py b/src/textual/drivers/windows_driver.py index 0899f65ef..26b155f06 100644 --- a/src/textual/drivers/windows_driver.py +++ b/src/textual/drivers/windows_driver.py @@ -58,7 +58,6 @@ class WindowsDriver(Driver): self.console.file.write("\x1b[?2004l") def start_application_mode(self) -> None: - loop = asyncio.get_running_loop() self._restore_console = win32.enable_application_mode() diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py index ac643663e..2d6736b08 100644 --- a/src/textual/layouts/horizontal.py +++ b/src/textual/layouts/horizontal.py @@ -18,7 +18,6 @@ class HorizontalLayout(Layout): def arrange( self, parent: Widget, children: list[Widget], size: Size ) -> ArrangeResult: - placements: list[WidgetPlacement] = [] add_placement = placements.append x = max_height = Fraction(0) diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index ccdcd1be3..f26e531c0 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -19,7 +19,6 @@ class VerticalLayout(Layout): def arrange( self, parent: Widget, children: list[Widget], size: Size ) -> ArrangeResult: - placements: list[WidgetPlacement] = [] add_placement = placements.append parent_size = parent.outer_size diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index c469b381d..d79e0764c 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -398,7 +398,6 @@ class MessagePump(metaclass=MessagePumpMeta): self.app._handle_exception(error) break finally: - self._message_queue.task_done() current_time = time() diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 048bb56e1..93139501d 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -170,7 +170,6 @@ class Reactive(Generic[ReactiveType]): getattr(obj, "__computes", []).clear() def __set_name__(self, owner: Type[MessageTarget], name: str) -> None: - # Check for compute method if hasattr(owner, f"compute_{name}"): # Compute methods are stored in a list called `__computes` diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index ec3cd4790..ad8a99e7e 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -92,7 +92,6 @@ class ScrollBarRender: back_color: Color = Color.parse("#555555"), bar_color: Color = Color.parse("bright_magenta"), ) -> Segments: - if vertical: bars = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", " "] else: @@ -190,7 +189,6 @@ class ScrollBarRender: @rich.repr.auto class ScrollBar(Widget): - renderer: ClassVar[Type[ScrollBarRender]] = ScrollBarRender """The class used for rendering scrollbars. This can be overriden and set to a ScrollBarRender-derived class diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 0be163d67..40282e03f 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -88,7 +88,6 @@ class DirectoryTree(Tree[DirEntry]): id: str | None = None, classes: str | None = None, ) -> None: - self.path = path super().__init__( path, diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index ee47b3801..278ab3a88 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -28,7 +28,6 @@ class _InputRenderable: def __rich_console__( self, console: "Console", options: "ConsoleOptions" ) -> "RenderResult": - input = self.input result = input._value if input._cursor_at_end: diff --git a/src/textual/widgets/_static.py b/src/textual/widgets/_static.py index 7d2059b3d..a9698a954 100644 --- a/src/textual/widgets/_static.py +++ b/src/textual/widgets/_static.py @@ -57,7 +57,6 @@ class Static(Widget, inherit_bindings=False): id: str | None = None, classes: str | None = None, ) -> None: - super().__init__(name=name, id=id, classes=classes) self.expand = expand self.shrink = shrink diff --git a/src/textual/widgets/_text_log.py b/src/textual/widgets/_text_log.py index 3b6f931f4..1c116c0b4 100644 --- a/src/textual/widgets/_text_log.py +++ b/src/textual/widgets/_text_log.py @@ -159,7 +159,6 @@ class TextLog(ScrollView, can_focus=True): return lines def _render_line(self, y: int, scroll_x: int, width: int) -> Strip: - if y >= len(self.lines): return Strip.blank(width, self.rich_style) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 2f3d073cf..1eee0da0f 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -281,7 +281,6 @@ class TreeNode(Generic[TreeDataType]): class Tree(Generic[TreeDataType], ScrollView, can_focus=True): - BINDINGS: ClassVar[list[BindingType]] = [ Binding("enter", "select_cursor", "Select", show=False), Binding("space", "toggle_node", "Toggle", show=False), diff --git a/src/textual/widgets/_welcome.py b/src/textual/widgets/_welcome.py index 3c8a6d1be..0ad5032dc 100644 --- a/src/textual/widgets/_welcome.py +++ b/src/textual/widgets/_welcome.py @@ -24,11 +24,10 @@ Where the fear has gone there will be nothing. Only I will remain." class Welcome(Static): - DEFAULT_CSS = """ Welcome { width: 100%; - height: 100%; + height: 100%; background: $surface; } @@ -44,7 +43,7 @@ class Welcome(Static): Welcome #close { dock: bottom; - width: 100%; + width: 100%; } """ From a5808db8b86f36252565ceb8a400176124fc184e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 3 Feb 2023 19:10:03 +0100 Subject: [PATCH 05/17] more docs and diagrams --- docs/api/scroll_view.md | 1 + docs/examples/guide/widgets/checker02.py | 6 -- docs/examples/guide/widgets/checker03.py | 26 +++---- docs/guide/widgets.md | 97 +++++++++++++++++++----- docs/images/render_line.excalidraw.svg | 6 +- docs/images/scroll_view.excalidraw.svg | 16 ++++ mkdocs.yml | 1 + 7 files changed, 111 insertions(+), 42 deletions(-) create mode 100644 docs/api/scroll_view.md create mode 100644 docs/images/scroll_view.excalidraw.svg diff --git a/docs/api/scroll_view.md b/docs/api/scroll_view.md new file mode 100644 index 000000000..461d9de6f --- /dev/null +++ b/docs/api/scroll_view.md @@ -0,0 +1 @@ +::: textual.scroll_view.ScrollView diff --git a/docs/examples/guide/widgets/checker02.py b/docs/examples/guide/widgets/checker02.py index 48b674ea9..f108de8c5 100644 --- a/docs/examples/guide/widgets/checker02.py +++ b/docs/examples/guide/widgets/checker02.py @@ -24,12 +24,6 @@ class CheckerBoard(Widget): } """ - def get_content_width(self, container: Size, viewport: Size) -> int: - return 64 - - def get_content_height(self, container: Size, viewport: Size, width: int) -> int: - return 32 - def render_line(self, y: int) -> Strip: """Render a line of the widget. y is relative to the top of the widget.""" diff --git a/docs/examples/guide/widgets/checker03.py b/docs/examples/guide/widgets/checker03.py index ccdab8ebf..03ca19381 100644 --- a/docs/examples/guide/widgets/checker03.py +++ b/docs/examples/guide/widgets/checker03.py @@ -23,42 +23,40 @@ class CheckerBoard(ScrollView): } """ - def get_content_width(self, container: Size, viewport: Size) -> int: - return 64 - - def get_content_height(self, container: Size, viewport: Size, width: int) -> int: - return 32 - - def on_mount(self) -> None: - self.virtual_size = Size(64, 32) + def __init__(self, board_size: int) -> None: + super().__init__() + self.board_size = board_size + # Each square is 4 rows and 8 columns + self.virtual_size = Size(board_size * 8, board_size * 4) def render_line(self, y: int) -> Strip: """Render a line of the widget. y is relative to the top of the widget.""" - scroll_x, scroll_y = self.scroll_offset - y += scroll_y + scroll_x, scroll_y = self.scroll_offset # The current scroll position + y += scroll_y # The line at the top of the widget is now `scroll_y`, not zero! row_index = y // 4 # four lines per row white = self.get_component_rich_style("checkerboard--white-square") black = self.get_component_rich_style("checkerboard--black-square") - if row_index >= 8: + if row_index >= self.board_size: return Strip.blank(self.size.width) is_odd = row_index % 2 segments = [ Segment(" " * 8, black if (column + is_odd) % 2 else white) - for column in range(8) + for column in range(self.board_size) ] - strip = Strip(segments, 8 * 8) + strip = Strip(segments, self.board_size * 8) + # Crop the strip so that is covers the visible area strip = strip.crop(scroll_x, scroll_x + self.size.width) return strip class BoardApp(App): def compose(self) -> ComposeResult: - yield CheckerBoard() + yield CheckerBoard(100) if __name__ == "__main__": diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index be7098232..80676284c 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -200,30 +200,28 @@ TODO: Explanation of compound widgets ## Line API -Working with Rich renderables allows you to build sophisticated widgets with minimal effort, but there is a downside to widgets that return renderables. -When you resize a widget or update its state, Textual has to refresh the widget's content in its entirety, which may be expensive. -You are unlikely to notice this if the widget fits within the screen but large widgets that scroll may slow down your application. +A downside of widgets that return Rich renderables is that Textual will redraw the entire widget when its state is updated or it changes size. +If a widget is large enough to require scrolling or updates frequently then this redrawing can make your app feel less responsive. +Textual offers an alternative API which reduces the amount of work required refresh a widget, and makes it possible to update portions of a widget (as small as a single character) without a full redraw. This is known as the *line API*. -Textual offers an alternative API which reduces the amount of work Textual needs to do to refresh a widget, and makes it possible to update portions of a widget (as small as a single character). This is known as the *line API*. +!!! note -!!! info - - The [DataTable](./../widgets/data_table.md) widget uses the Line API, which can support thousands or even millions of rows without a reduction in render times. + The Line API requires a little more work that typical Rich renderables, but can produce very power widgets such as the builtin [DataTable](./../widgets/data_table.md) which can handle thousands or even millions of rows. ### Render Line method -To build an widget with the line API, implement a `render_line` method rather than a `render` method. The `render_line` method takes a single integer argument `y` which is an offset from the top of the widget, and should return a [Strip][textual.strip.Strip] object which contains that line's content. -Textual will call this method as required to to get the content for every line. +To build a widget with the line API, implement a `render_line` method rather than a `render` method. The `render_line` method takes a single integer argument `y` which is an offset from the top of the widget, and should return a [Strip][textual.strip.Strip] object containing that line's content. +Textual will call this method as required to to get content for every row of characters in the widget.
--8<-- "docs/images/render_line.excalidraw.svg"
-Let's look at an example before we go in to the details. The following Textual app implements a widget with the line API that renders a checkerboard pattern. This might form the basis of a chess / checkers app. Here's the code: +Let's look at an example before we go in to the details. The following Textual app implements a widget with the line API that renders a checkerboard pattern. This might form the basis of a chess / checkers game. Here's the code: === "checker01.py" - ```python title="checker01.py" hl_lines="12-30" + ```python title="checker01.py" hl_lines="12-31" --8<-- "docs/examples/guide/widgets/checker01.py" ``` @@ -233,11 +231,13 @@ Let's look at an example before we go in to the details. The following Textual a ``` -The `render_line` method above calculates a `Strip` for every row of characters in the widget. Each strip contains alternating black and white space characters which form the squares in the checkerboard. You may have noticed that the checkerboard widget makes use of some objects we haven't covered before. Let's explore those. +The `render_line` method above calculates a `Strip` for every row of characters in the widget. Each strip contains alternating black and white space characters which form the squares in the checkerboard. + +You may have noticed that the checkerboard widget makes use of some objects we haven't covered before. Let's explore those. #### Segment and Style -A [Segment](https://rich.readthedocs.io/en/latest/protocol.html#low-level-render) is a class borrowed from the [Rich](https://github.com/Textualize/rich) project. It is small object (actually a named tuple) which bundles text and a [Style](https://rich.readthedocs.io/en/latest/style.html) which tells Textual how the text should be displayed. +A [Segment](https://rich.readthedocs.io/en/latest/protocol.html#low-level-render) is a class borrowed from the [Rich](https://github.com/Textualize/rich) project. It is small object (actually a named tuple) which bundles a string to be displayed and a [Style](https://rich.readthedocs.io/en/latest/style.html) which tells Textual how the text should look (color, bold, italic etc). Lets look at a simple segment which would produce the text "Hello, World!" in bold. @@ -251,27 +251,86 @@ This would create the following object: --8<-- "docs/images/segment.excalidraw.svg" -Both Rich and Textual work with segments to generate content. A Textual app is the result of processing hundreds, or perhaps thousands of segments. +Both Rich and Textual work with segments to generate content. When you run a Textual app you are seeing hundreds or perhaps thousands of segments combined together. #### Strips -A [Strip][textual.strip.Strip] is a container for a number of segments which define the content for a single *line* (or row) in the Widget. A Strip only requires a single segment, but will likely contain many more. +A [Strip][textual.strip.Strip] is a container for a number of segments covering a single *line* (or row) in the Widget. A Strip will at least one segment, but often many more. -You construct a strip with a list of segments. Here's now you might construct a strip that ultimately displays the text "Hello, World!", but with the second word in bold: +A `Strip` is constructed from a list of Segment objects. Here's now you might construct a strip that displays the text "Hello, World!", but with the second word in bold: ```python segments = [ Segment("Hello, "), - Segment("World", Style(bold=Trip)), + Segment("World", Style(bold=True)), Segment("!") ] strip = Strip(segments) ``` -The `Strip` constructor has a second optional constructor, which should be the length of the strip. In the code above, the length of the strip is 13, so we could have constructed it like this: +The first and third strip omit a style, which results in the widgets default style being used. The second segment has a style object which applies bold to the text "World". If this were part of a Strip it would produce the text: Hello, **World**! + +The `Strip` constructor has an optional second parameter, which should be the *cell length* of the strip. In the code above, the length of the strip is 13, so we could have constructed it like this: ```python strip = Strip(segments, 13) ``` -Note that the length parameter is _not_ the total number of characters in the string. It is the number of terminal "cells". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to leave the length parameter blank. +Note that the cell length parameter is _not_ the total number of characters in the string. It is the number of terminal "cells". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to leave the length parameter blank. + +### Component classes + +When applying styles to widgets we can use CSS to select the child widgets. Widgets rendered with the line API don't have children per-se, but we can still use CSS to apply styles to parts of our widget by defining *component classes*. Component classes are associated with a widget by defining a `COMPONENT_CLASSES` class variable which should be a set of strings containing CSS class names. + +In the checkerboard example above we hard-coded the color of the squares to "white" and "black". But what if we want to create a checkerboard with different colors? We can do this by defining two component classes, one for the "white" squares and one for the "dark" squares. This will allow us to change the colors with CSS. + +The following example replaces our hard-coded colors with component classes. + +=== "checker02.py" + + ```python title="checker02.py" hl_lines="13-15 18-25 37-38" + --8<-- "docs/examples/guide/widgets/checker02.py" + ``` + +=== "Output" + + ```{.textual path="docs/examples/guide/widgets/checker02.py"} + ``` + +The `COMPONENT_CLASSES` class variable above adds two class names: `checkerboard--white-square` and `checkerboard--black-square`. These are set in the `DEFAULT_CSS` but can modified int he apps `CSS` class variable or external CSS. + +!!! tip + + Component classes typically begin with the name of the widget followed by *two* hyphens. This is a convention to avoid potential name clashes. + +The `render_line` method calls [get_component_rich_style][textual.widget.Widget.get_component_rich_style] to get `Style` object from the CSS, which we apply to the segments to create a more colorful looking checkerboard. + +### Scrolling + +Line API widgets require a little more more work to handle scrolling. + +A Line API widget can be made to scroll by extending the [ScrollView][textual.scroll_view.ScrollView] class. We also need to manage the following details: + +1. The ScrollView requires a *virtual size* which is the size of the scrollable content, and should be set via the `virtual_size` property. If this is larger than the widget then Textual will add scrollbars. +2. We need to update the `render_line` method to compensate for the current position of the scrollbars. + +Lets add scrolling to our checkerboard example. A standard 8 x 8 board isn't really sufficient to demonstrate scrolling so we will also make the size of the board configurable, and set it to 100 x 100, for a total of 10,000 squares. + +=== "checker03.py" + + ```python title="checker03.py" hl_lines="26-30 35-36 52-53" + --8<-- "docs/examples/guide/widgets/checker03.py" + ``` + +=== "Output" + + ```{.textual path="docs/examples/guide/widgets/checker03.py"} + ``` + +The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). + +The `render_line` method gets the scroll offset via the `scroll_offset` property. This attribute is an [Offset][textual.geometry.Offset] that indicates the position of the scroll bars. It starts at `(0, 0)` but will change if you move any of the scrollbars. + +
+--8<-- "docs/images/scroll_view.excalidraw.svg" +
diff --git a/docs/images/render_line.excalidraw.svg b/docs/images/render_line.excalidraw.svg index 0fe5edfa2..591956da0 100644 --- a/docs/images/render_line.excalidraw.svg +++ b/docs/images/render_line.excalidraw.svg @@ -1,6 +1,6 @@ - + - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlT29hcdTAwMTL9nl9BMV/mVVxymtt9107V1CsggVx1MDAxMFx1MDAxMpZcdTAwMDAhydRcdTAwMTQlbNkoyFx1MDAwYrZYp/LfX1+HWPJcIi9gg/Mq/sCixbq6Oqf79HL174ulpeX0tlx1MDAxOS2/XFxajm5KYVx1MDAxMpdb4fXyXHUwMDFmfvtV1GrHjTrvws7/7cZlq9Q58ixNm+2Xf/5ZXHUwMDBiW+dR2kzCUlx1MDAxNFxcxe3LMGmnl+W4XHUwMDExlFx1MDAxYbU/4zSqtf/rf+6EteivZqNWTltBdpGVqFx1MDAxY6eN1vdrRUlUi+ppm7/9b/5/aenfzs/c6FpRKVxy69Uk6pzQ2ZVcctBcdTAwMDD1b91p1DuDNUaRVcaZ7lx1MDAwMXH7XHUwMDE1Xy6Nyry3wkOOsj1+07K+Ofp8eHXS3LFHt3fHn1x1MDAwZrU6KWN21UqcJFx1MDAwN+lt8n0mwtLZZSs3pnbaapxHx3E5Pfsxcbnt3fPaXHKehOysVuOyelaP2v7+obu10VxmS3F6y9uc6G78Plx1MDAwNy+Xsi03/N+KXHUwMDBlSElcco60sJZcdTAwMDCzK/vzQcuAJFx1MDAxOGVRXHUwMDE4YbTqXHUwMDFi13oj4SfB4/pccpxR4Wk2stOwdF7l4dXL3WPSVlhvN8NcdTAwMTY/r+y46/s7llZcdTAwMDYoJKDu7jqL4upZ6mdcdTAwMDNNoDU5LY0mcoQ2XHUwMDFiRtR5XHUwMDFjnVx1MDAxMaIlm92dv3hzq9xBxj/5XHSrl+8nrH6ZJNl4/Y7XOTRl51xcNsthen9cdTAwMTmrXHUwMDE136mTaDNcXCRx/bz/65JG6TxcdTAwMDNKZ+u3P1x1MDAxZVx1MDAwMFBcdTAwMTAgi1x1MDAxMOo0II9mXG6EJudvL07OWttm//VeYuBcXO3i7UZcdTAwMDFCS61Gu71yXHUwMDE2pqWzeaNcdTAwMTTEWJiSXGLIXGJGoWVcdTAwMWOCQtlcdTAwMDNTS1x1MDAwMVx1MDAxMfFUOKl4PnJT1lx1MDAwZlPR+TxcdTAwMWOmWqnASJRKXG5cclx1MDAwMEPAKo1cYlx1MDAxY1xiUspITyw0XHUwMDAzYJWkXHUwMDE5SFK7p1x1MDAwMKtcdTAwMDRcdTAwMGLOPlx0WG3OevRhXHUwMDE1XHUwMDE0gHDOkptcdTAwMTiselx1MDAxYtdLlZ3rXHUwMDEwV48/X8sqnCTtXCJz2lx1MDAwN7jngylcdTAwMDRsXHUwMDFhpLFCailcdTAwMTBcdTAwMDZgqlx1MDAxYym0qK11QPOEqVxmXHUwMDA0XHUwMDE4jU5bYLPoXHUwMDA2cYouMEYwX4xAI1x1MDAxOdODOCWpjVNcdTAwMDaexKiyXHUwMDAxZ/czK5xGSVx1MDAxMjfbw32+1kUoZSdI2mmaXHUwMDE4o/tblavt2/btu8b5fvpqq7Xd3jy7eFxiRuHpMOpRKEGxXHQzXHUwMDBlnKBejDpcbrRlMEhEIZxcdTAwMTbFXHUwMDFlf1x1MDAwMoz+Vlx0NWpcdTAwMWPEJ8hAXHRETVx1MDAwNvlcdTAwMTfbbDVcYlDAQHtcdTAwMTHGRlSglKhcdTAwMDa8PqNcdTAwMDadcvIp8GlJSmFmZkdH4NOKQk1cbkDEXCIsr1x0xiFcdTAwMTRL9epd8ql8UD9z7037IPz62Vx1MDAxNrn8RUGo5icvhLLOgkFgZ9mLUFx1MDAxNzAkrHYktGJcdTAwMDSbR1wi9FRcYj0vhEq2v1Yw1/6/XHUwMDEwalxuNSmwN0GSrOQnRujx3vnd3seztUR8NDtbu69cdTAwMGU+hSBcdTAwMTdcdTAwMWOhqFx1MDAwMmWV0lx1MDAxYVxmgGFcZuheiNrAXG6Bjkhaw/B4lKP/XHLwlEXtvCDK4yNhnTA/XHUwMDFmREeKUeegXGKkXHUwMDE2LT9cdTAwMWE1ReBUPtjcK5feXHUwMDFkXHUwMDFlfbXXl+9vtnVaufo4U4yWw/ZZVFx1MDAwMFLxIJCKgE2nZFx1MDAwNUhKWFx1MDAwZVVcdTAwMTimPSBcdTAwMDVNXHUwMDAxKFwiRFx1MDAwNrHkR6NcdTAwMWaFUsviqYI4xNn7iEhbdJKU4YB1aIRvOIBTqLS0SvDPXHUwMDFjlH9YUmUsx3dmXHUwMDFhmP5cdTAwMDBMXHUwMDA2XHUwMDE5eb/lWzF6u+dkZ3dh8OHi6vqotH1z/mXjjbn7dLu+d4U33Vx1MDAxYu7BZthqNa6Xu3u+3f81Wlx1MDAwMIOUs+JGXHUwMDFh3aTDaJFPXFz0225kuYfscyfnxWf68O44OXh9VHVrlcu1la33unT4sJTXXHUwMDEzWm/JXHUwMDAyg8VcdTAwMDOLXHUwMDBidD5Igj5eIFx1MDAwN/DGXHUwMDAy81x1MDAwNlBcdTAwMDL1x4+zXHUwMDBi0zCXx8i4oPuxzyzV3tnM0EY/XHUwMDE3yontP1x1MDAxYpspUJ6BqVFPXHUwMDBm4jt/XVx1MDAxND1bN8JanNz24KFcdTAwMDP/l52ZrkZpwNNfjlonfLXo99u/xH/yj6xcdTAwMWT5zf5s13P6alx1MDAxMlc9Y5aTqNJLpTQuhUl3d9poZntLPJyQv661Ve6/rUYrrsb1MDlcdTAwMWMztFx1MDAwN9FaymJak2GpLJCySVx1MDAxYpt6OSyJ7Z3j2Lb23Ea7XHUwMDE27tLm262fgNZcdTAwMTAwY31cdTAwMTJcdTAwMTmlQsh8yG1cdTAwMDczLJfY3Vx1MDAxOWFcYpx1OVx1MDAxMM6c1sPygoO0Zt2Filx1MDAxZF5mX+bJ6ovSOl1cdTAwMWVcXFx1MDAxY5TIJW+2w8r51sX705mxmvWFy+jzLKyGxWU1PJDVXG5HOGtgXHUwMDBmpmQutFx1MDAxY5v9t1W8S/ejq4+7a+vxp0q8sofXP1x1MDAwMatZPXJcdTAwMWOtOeBHpjj1hlrIpFx1MDAwNyeNXHUwMDE2IDjMcY9TsWO89ZBcdTAwMDBriLeWRGBYwj5ccq9cdTAwMWKlL2Hrc/XNUby3c0tcdTAwMDf753Sxvzs7XrOdnCYpO1x1MDAwN17j4vJcdTAwMWHH8Pr7hFx1MDAwZiE22Fx1MDAxMcxmLDsjlZ6c2aNcdTAwMTXbojJcdTAwMWJQXHUwMDA1TGpcdIZcdTAwMWShhlxcKb7zXHUwMDA1xlx1MDAwNEZIjkhAXHUwMDE4a3N7Z81rllxmgZIojbLaKeNy5eUuzW3AY+DwXHUwMDEzUbAlUjY3mlx1MDAxZlx1MDAxNWi+XHRkXHUwMDE5Nlxy61x1MDAwN1x1MDAwM1S83zJcIkCdi7hup2ErXYvr5bhe7Vx1MDAxZNh9h8XWXHUwMDA08Z6/57DJx+lAslx1MDAxZfDRPIFPUeVcdTAwMGWoNEqX/jZWXHUwMDA0XHUwMDFmozjgXHUwMDE1XHUwMDAyNJt1qaxcdTAwMWG4dybb+DGNzs10x6RcdTAwMDJcdTAwMGX70Fx0K1x1MDAxZIByomBQWlx1MDAxYbbfzjdcdTAwMTOgXCI3OKYkbKfrjVotTnnq91x1MDAxYXE97Z/izlxcrnrqn0XhgGHhe8rv67dcdTAwMTFN/429Rj/7aymjUOef7t///DH06GJk+89cdTAwMDCms697kf89tXWjnMXvM27KIbFHUZOnXHUwMDE4RuvWxbVtLrDagNOs4ZC07a2yoaCAxYzPXHUwMDBlk1VujilcdTAwMDbWiXwlQYJcdTAwMTWU0EbYIcZcclx1MDAwMmdRKKOUYjpKUplr7Vo3zSjik6cptc3cuqGRuVr07K3b6LC3z05cdTAwMTBJr0qN1Fxm9yyp1rU3NkDtbaAlXHUwMDBiqI2mgXufyLrFr5Tcfp2EXHUwMDE3sqbOpfy6cX1T2y1cdTAwMThcdTAwMTMqjdbDylk2pzJcdTAwMTNcdTAwMTC5QVx0ckBsXG6kNlxmwsFB/Vxc5q1cYtudvVx1MDAwM6ielX1j/qv+zT9cZlx1MDAxY1+RXHUwMDFmunNTWLjRXG5+cS1cdTAwMWNcdTAwMDb8XHUwMDAwlE/bO3anqle+oXZcdTAwMDFcIu9k22dcdTAwMTVcbphfXFxcdTAwMDYgXHUwMDAzXHUwMDAwa1x1MDAwNFlhnTFmSPaFrTGjwLJcdTAwMWRhU0yoXHUwMDA36lx1MDAwYmBcdTAwMWPfhbR2mqas2Vs4ma+tzt7CjU5cdTAwMDHkrIlcYvyQhEGwguWZyTXN5Fx1MDAxNJXivVx1MDAwNoxiU0PqoVx1MDAxNq7S/lK3Z+dX71x1MDAwZk/v1j7U6u06bSbDx6S0XHUwMDE3KlqTIYRcXMCQXHUwMDEznlx1MDAxNiSw3tFOkFwio39u+1ZcYmz/WVx1MDAxOcD0lPatsEQkXHUwMDBiXHUwMDFiUFhcdTAwMWQ7R9O0nH7Q5eZXiCv66HhXvq9cdTAwMWRUV6vh6oybokfVTlx1MDAxZtRcdTAwMTWtKFx1MDAxMFqwW1eCXHUwMDAzXHUwMDA2tH1cdTAwMDVcIoOBN3uoOTz1TnSOKSeX3XSWcspcdTAwMDbbNV6CXHThi+tTXHUwMDE4r4ennJrutLpcdTAwMGLXglx1MDAxZn1cZrXTt5eVzcPV5Tw2XHUwMDFmUVx1MDAwNtXeWT825Vx1MDAwNKZnazfllNnWXHUwMDFmKaeDtFx1MDAxNTd//7tcdTAwMWRV/a3/sdT9I1xign9cbjJPpudb5p15XHUwMDFhM8KRXHUwMDE0XHUwMDFm2SFBrjBcdTAwMDcl2WFcdTAwMWLjYPJcdTAwMTTUaJ26mFx1MDAxZFx1MDAxMiyZXHUwMDE1WuGYOuznkC1bX6tcdTAwMTmQXHQ0XGLtrFx1MDAwMsFm4HF9PHPukGDlyVx1MDAwMac09DhcdTAwMDXzyFx1MDAxNol5lpmc8vX6p2lcdTAwMWZcdTAwMDKhXG6TXHUwMDE44PhRXHUwMDAyu93Jm9lHi5xcdTAwMDWlh1xmkNhcdTAwMDc64dtshctJ447ClypcdTAwMDDBXHUwMDFhS1x1MDAxOO8rXHUwMDFm14Y5Z26AsnyE1e5Z24fmWapxXHUwMDFjc1x1MDAxMsCsuFGoXHJdcXMya1SGis7Vvca2XHUwMDBm7ey+Wa2JVbprn8qtV4d3ur3eXFx4cehcdTAwMDKWKOTDXGaSyvSFvVx1MDAwMlx1MDAwMo5cIrVi1iiL+nG0mIk2lKCcT2nPTFx1MDAxYY4y0IpcdTAwMWM9KID9Jd4m7/UpXlx1MDAxMmhcclqj5Vx1MDAxNFx1MDAxNNy6OUluV6rHuGJaXHUwMDFm4NPm2+al2lt4XG6awHJ879dcdTAwMDEqYuXWl1xclzaQzvpYXHUwMDE1XGa7apxjXHUwMDAz32RcdTAwMWN00oBcdTAwMTb6SVx1MDAxNqp8d1x1MDAwNL9cdTAwMDKo+XIwt6KyP0WCWrEnzFx1MDAxNSrGUVx1MDAxMOyO2Wx93Ph8udZ6Xd6/Wf2y594+qzZcdTAwMWNPQFx1MDAxOVxiTSiZX9pIYUxfr51cZlhxSak5ZNLWWFPsXHUwMDA1J1g3PkJcdTAwMWOCXHUwMDFltrJxsCWHdaomO11otKj5XHUwMDEx31xi/3QtOe+YY0ure1tLx51cdTAwMDaYRWjF6Vx1MDAxZtJIXHUwMDAyXHUwMDE3XHUwMDE3cUYsYpJoWcTRXHUwMDE0XHUwMDFkOKNcdTAwMWb5nFx1MDAxNys/iMLAUZNwQCT9anW/YLmHwlx1MDAxY3WRr4Zw0GVcdTAwMTSCUXOjMFx1MDAwNYTWknFIXHUwMDFjn1x0NaQlnny/vkFnObwgXHUwMDA0kZuke1x1MDAwN8tcdTAwMDGqtbNskJ9cdTAwMGJcdTAwMDMnrNKMdlxiS/mKiOis7idJfPdgncDBMlxyYGBJcEDCqoBcdTAwMDW6+1FcdTAwMDadskwzOpG/1FuI9kVvK3iiSFGuWNpcdTAwMWSTXHUwMDBifMOmlpLHbCzan7xcdTAwMGVdXGJh/1x1MDAxOVx1MDAwMG/2dS/yv1x1MDAxZpamyivQgcZ/we53mj7C6t715/2tVXVU0lx1MDAxOFx1MDAxZlbLpdKnXHUwMDBm5WdVXCJ6glx1MDAxY67TQlx1MDAxMlx1MDAwNz2ChYjvWO2VXCJWeKmiXGZHTSSketx69lx1MDAxMWkq3SnHOVx1MDAxZYhfpVx1MDAwNjCsV3hsnspJpoPLv0fj/yxN5ZdcdTAwMDbr2b07p7Bx3lx1MDAxNb81R1xu7H2pxThWbOxcXNXWV1x1MDAxMnXSbJ7WTva3dr/S6eYzh8hqrELnXHUwMDE4XHUwMDE4hUNk0Fx1MDAxYt/Q2Mdcbun7z7T2K2G1yq9cdTAwMWV6tlx1MDAxMiZTVEF+1c78XFw4oWK/RFOg8FeM/FBcdTAwMDdcdTAwMDXSXHUwMDE1UdGvsFx1MDAxNEZbOTlcdTAwMTe/bJ6+XHKbXHUwMDA3d28+XHUwMDFjmbXD+MvtdvV6ti9cdTAwMGKYh4cygpTtLDtcdTAwMDOHorebgJ1WYDlQXHUwMDE2llU2MiFcdTAwMTe6yFxiyFx1MDAxNlx1MDAwM5Sc/zrsUeyVOt/HOi9cdTAwMWZcIlxugauclkhcdTAwMDCT1//e1Y/dUSk+Xr9z9crhyWbra7PZWHhcdTAwMWZCXHUwMDAxaybDN0vsRYj6gFx1MDAwYqyspOr0wVwi+ddQPLtcdTAwMGbRvulVXHUwMDFhN7uVVyN9XGLz+bErmX/5kIl8yIhcdTAwMTfLsbBHSTDFi+VWReVqy1RcZiRXeq/0Pv566q6KSlx1MDAxZU/jQ8Yy0SdjgONKwzwzXHUwMDFj6vfmaqSSbNlccvnXfKAzrn9cXFx1MDAwYuVBWHBKwdHa8zpcdTAwMTBcdTAwMDdi7rVySYWoVWA0+ZU3XHUwMDEzg/a2vXF516Aj65JqUrYnXHUwMDFi+u7t60V3IJqDc0SD/MDJZ+dUXHUwMDFmbFn5MJJASHRkzLOX6Tg0XHUwMDE1pK1+klo5+1x1MDAwZmaanlx1MDAwMoS//Mf3XGZDZ06Xw2bzIOVcdTAwMTntmlx1MDAwN35Wcfl+WrLrLF/F0fXasFx1MDAxN791Pv5bO7z2XGaKOrbm24tv/1x1MDAwM0RNWtcifQ== + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9nl9BsV/2Vlx1MDAwNe109zxTtXVcdTAwMGJIIISER4BcdTAwMTBya4tcdTAwMTK2bFx1MDAxNORcdTAwMDe2eG7lv99cdTAwMWWHWPJDxjY2ce5d71x1MDAwNowk2+OZc7pPP0Z/v1haWk7vmtHyq6Xl6LZcdTAwMTQmcblcdTAwMTXeLL/0x6+jVjtu1PlcdTAwMTR2/m43rlqlzpXnadpsv/rjj1rYuojSZlx1MDAxMpai4DpuX4VJO70qx42g1Kj9XHUwMDExp1Gt/W//cyesRX82XHUwMDFitXLaXG6yXHUwMDBmWYnKcdpoff+sKIlqUT1t87v/h/9eWvq78zM3ulZUSsN6NYk6L+icylx1MDAwNqjR9Vx1MDAxZt1p1DuDtWhJkJa6e0Hcfs1cdTAwMWaXRmU+W+EhR9lcdTAwMTl/aFndXHUwMDFlnVx1MDAxY16fNnfM0d398cmhkqdlzD61XHUwMDEyJ8lBepd8n4mwdH7Vyo2pnbZcdTAwMWFcdTAwMTfRcVxcTs/5vOw73n1du8GTkL2q1biqntejtv/+0D3aaIalOL3zX0J0XHUwMDBmfp+DV0vZkVv+a0VcdTAwMDVOklx1MDAwMuuUMMZcdJd9sn89KFxurFJGgUGhrXR941pvJLxcdTAwMTI8rt9UhcolmY3sLCxdVHl49XL3mrRcdTAwMTXW282wxeuVXXfz8I3JUICCXHUwMDAwVffUeVx1MDAxNFfPU1x1MDAwZiPUgVLOKtLKOevQZMOIOstcdTAwMDFaXHUwMDFhRONM9u38hze3ylx1MDAxZGT8lZ+wevlhwupXSZKN159404+mPKJyK920Z9VduFx1MDAxMVxmiFx1MDAxOGpn764qm4er3e/UXHUwMDAzv7DVatwsd898e3iWjeiqWVx1MDAwZdOHL2GURGLgWYvd80lcXL/oXHUwMDFmbNIoXWQw7Fx1MDAxY/32clxu+IOAQvzzSljhQMmx8Z9cXLy7PD1vbev9N3uJhlx1MDAwYrmLd1x1MDAxYlx1MDAwNfgvtVx1MDAxYe32ynmYls6LOIAz4lx1MDAwMIhHSeBE4LRgiFx1MDAxYqFcdTAwMDXwXHUwMDFh9JDAuMA5XHUwMDA3vCokrdVAhSRcdTAwMTCdx/QkUFJcdTAwMDaakCRcdFx1MDAwNVx1MDAwMEOoQFpcdTAwMDRcdTAwMTaYplKTpy3qXHUwMDAxKpBTXGYkUnZmVFx1MDAxOFx1MDAwMVaDRln9LGA1aFx1MDAwYrHqnCG2XGZkx1x1MDAwNqvaxvVSZecmxNXjk1x1MDAxYqrCadIuMtZ9gPt5MIWA0JA2glx1MDAxNFx0hFx1MDAwMZhK6yRcdTAwMWFUxlhw84QpXHUwMDA1XHUwMDAytEKrXGaw0bWDOEVcdTAwMWJoLZgvWqAmxvQgTlx1MDAxZCn2KFx1MDAxYWZnslx1MDAxZsOpmVx1MDAxNU6jJImb7eGKQkNcdTAwMTFKnbcsykpcdTAwMWNcdTAwMWKk+1uV6+279t37xsV++nqrtd3ePL+cXHUwMDA2pPB8IPUwJJBsw7RcdTAwMDX2IL0gtS5QhtFAiEJYJfqFzkQg/a1cdTAwMTIqVDhcYlCgQFxuROU08i822nJcdTAwMTChgIHSWjJcbkEgXHUwMDExylx1MDAwMVEh2bxaySrwf1xuoCbnVvpcdTAwMDCK1pFGY8dcdTAwMDcolurV++Rz+aB+bj/o9kH49cRcdTAwMTS5/EVcdTAwMDGo4oVcdTAwMTdCXHUwMDFha0AjsLPsXHUwMDA1qFxyXHUwMDE4XHUwMDExvFx1MDAxNk4oyVx1MDAwMNZPXHUwMDA06JlcdTAwMTBqXlx1MDAwMCW2v0Yw1Z5ccqD2OVx1MDAwMKpcdTAwMGI1qXKs0YhyjHxcZqDHe1x1MDAxN/d7n87XXHUwMDEy8UnvbO2+PvhcdTAwMWNcdTAwMDItOEBRXHUwMDA20kipXHUwMDE0aFx1MDAwMM1cdTAwMTBQvVxiNYFcdTAwMTGCueqIp4qVwJNcdTAwMTBcbnjGmnZeXGLl8TlhrNC/XHUwMDFlQkdqUetcbr08XHUwMDAwSZBKuPFBWj7Y3CuX3lx1MDAxZlx1MDAxZX01N1dcdTAwMWZut1Vauf70XFwgXHUwMDE1U4FUXHUwMDA0QILYO3JULpSPm7BcdTAwMDekoFxcXHUwMDAw0jlEXHUwMDA2MVx1MDAxOYbIk1BqnFx1MDAxMlx1MDAxNcQhrt5cdTAwMDdEikNcdTAwMDNyUqNcdTAwMWOePtBcdTAwMWO/SZSKjFx1MDAxNPwzXHUwMDA35Vx1MDAxZoZUasPhnZ5cdTAwMDSmWVrgXHUwMDA3ZOjhyLdi9HZfMySp8PHy+uaotH178WXjrb7/fLe+d4234yVcdTAwMTVejnrfuSYrXGZcdTAwMWJkSbPiXFxcdTAwMWHdpsPohrYw9EMjrVx1MDAwNDOBSzhxXHUwMDFm31x1MDAxZidcdTAwMDdvjqp2rXK1trL1QZVcdTAwMGWnS9M9o1Mgli0sSViyoPWhXHUwMDE39NFcclx1MDAwMz5swElcdTAwMDFI4Pqj0tlcdTAwMDV/mMuOZFx1MDAxNFP9lGLyK+/DZmj650SeR0HOVoVtuppcdTAwMDDkXHUwMDE5mFx1MDAxYfX0IL7vXHUwMDAwVfRcdTAwMWPdXGJrcXLXg4dcdTAwMGX6X3Vmulx1MDAxYaVcdTAwMDFPfzlqnfKnRb/f/Sn+lV+yduRcdTAwMGb7V9uel68mcdVcdTAwMTNmOYkqvUxK41KYdE+njWZ2tsTDXHT57Vpb5f6v1WjF1bhcdTAwMWUmh49cZm0qVlNxqFxmXHUwMDFjXHUwMDBikdMwfvJRXHUwMDFklsT2znFsWnt2o11cdTAwMGJ33ea7rV+C1Up4SrNzXCKle4NlNq1cdTAwMDEosFx1MDAxNkmwi2NcdTAwMTU2P1ZcdTAwMGZLNlx1MDAwZbKa1Vx1MDAxY0p2o5l5mSepL0vr7urg8qDkbPJ2O6xcXGxdfjibXHUwMDE5qVnTimzZflxuqWFxSVxyU5JajojehHLkI5qxSZ2YKt6n+9H1p9219fhzJV7Zw5tfgNQqQMGKmONcdTAwMTFcdTAwMTamprekhopcdTAwMDJhQLP168R4pm9gs/TUQ2K2IZ6anOPh2GdcInWj9CVsnVTfXHUwMDFlxXs7d+5g/8Jd7u/OjtSan/1cXFLj4pJcdTAwMWFcdTAwMWYh9fdcdFx1MDAxZsJqsLk0Wb+vJiU5XG5cdTAwMTg/aThaqy0qq1lWM29cdTAwMWRcdTAwMWFAzf6YSd6rwDWfVtrn45RcdTAwMDUgnJ9cdTAwMDJcdTAwMDewgVWGPOJRO8rlKLPUXGZcdTAwMDRoXHUwMDA1x2eWQ3Difznp8KNkzr5cdTAwMWOJ9CS0XHUwMDFmXGZ68eHIiKB3XHUwMDE0X0lLmspcdLfTsJWuxfVyXFyv9lx1MDAwZeyhJWRrjGCvw/DSlVx1MDAxZuWKXGKUlZJNtjI8JG0zseVnJmzyNTqwqKzzVXOSzFxioVx1MDAwNr480+3xQX1U5eZXiCvq6HiXPtRcdTAwMGWqq9VwtWBQXHUwMDA2wPB/1pLjNVdcdTAwMDNjXHUwMDAyXG5cYqWUwvjAz6EzXHUwMDAzY0rCdrreqNXilOd+r1x1MDAxMdfT/jnuTOaqJ/95XHUwMDE0XHUwMDBlmFx1MDAxNv5O+XP9VqLp37HX7GfPljJcdTAwMWF1/ug+/+vl0KuLsf39bD+qs/d7kf89uYFcdTAwMTNS9Fx1MDAxZv5h4CRcdTAwMThfUp+guDxauC6uhbNcdTAwMDFLe9ZcdTAwMDO+PFwiOf7qXHUwMDE1LiBcdTAwMDOn0WhpNIl8d8LMXHJcdTAwMWPKwFdg0DphhVOZQ8/6IJitvCp8gXS+ijOsJUghaWEmalx0mrl94zHgJL08k9q30WFvzpSIXHUwMDAwhFx1MDAwNuGs9Wl9XHUwMDBlrVx1MDAxNeSu+m5LePk77S+KULMxXHUwMDE5/OpjWbeTnd23qzWx6u7bZ7T1+vBetdebw4eEmlx1MDAxOExaW7Ik+HPNwJBAXHUwMDA0JFx1MDAxNS8w+1XprFx1MDAwNPdrm7dcImT7x8ogqGdm3vKxwEDzXGbrXHUwMDE29M1EY9u30Vx1MDAxYX5x7Vx1MDAxYrJEQ1x1MDAwNGRWSqFtb1x1MDAwZVx1MDAxNTXbN+NcdTAwMWJalCAgkvOLzNin+5KEVk5LadlMXHLpoFx1MDAwMVx1MDAxNVx1MDAwMFx1MDAwM1x1MDAwNSQrXHUwMDEydJZoiIWz1lxuMmaSTq+ZW7ipI64xLdzoXHUwMDFjQI85YelGzCpjkWWEM4NcdTAwMDbO16TYhnhcdTAwMDPoW+zUYMVmLFx1MDAwYrd1e5rcrVSPcUW3PsLnzXfNK7k3fEhsctF6YUasZyzpIUNcIoau0lxuoNP++ovbt0Jg+8fKIKYnNHCFWSco7lxyRI7IWDVOXHUwMDEyoI7U51x1MDAxM5m3wu7Actg+j2bZyc3YXHUwMDE2qHjWmY/ev/dFp8h+lGkhnWKZl/dcdTAwMDYzzzrl2oeyrFM21q7xXHUwMDEyXGZcdTAwMDRHM+yu6mJptkXQl6Ped751J2JdlKVTp8tmge452s1mZaWGXHUwMDFm2ayDtFx1MDAxNTd//087qvopfbnUfVx1MDAxMlx1MDAwNMFfXHUwMDA1SS3d8y7zTmo9MsKRpmNkP4fLtSn221x1MDAwZtZGPvKewH7EryVtv0nCS6rJXHUwMDBioq9cdTAwMWI3t7VcInm0IP1cdTAwMWMrXCKQaIT1qlx1MDAxM1xmWtvfXHUwMDE3XHUwMDA3Tlx1MDAwN1xuXHUwMDA0XHUwMDBiVO8/zdOa4Ofdz+FNvlx1MDAwNtLuacroiVxyXHUwMDFk8yxfkW9cdTAwMDM3z9PsxIFjYfLXi+lOz8H4RZ1K+0vdnF9cXH84PLtf+1irt+tuM1l4dqBwznI4L1x1MDAxOXhGqL6aXHUwMDBlSfa+TivB/6N4Ws/onLnB4ouv6Nm18Vx1MDAxM7gxzypcdTAwMTD5lqBcXNpnTk1JJFx1MDAwYiud2vq9RGqCRurRSZNcdTAwMDWVnDogjUL4rVNWXHSw/d1cdTAwMGJcIjDo87iW8WjNXHUwMDFjXHUwMDBinWNKTuLoz29cdTAwMTN6XHUwMDE2xTlX62+F31fzjzKciTIs7k8q7jpcdTAwMTSKpFx1MDAxNdaNnzNcdTAwMWKdNFhQhqtcdTAwMDBcdTAwMTUq0dnAxXymfrdnXHUwMDAyXHJW++SF1ajnV1x1MDAxMVx1MDAxOJPhljQoXprniSnn6sNcdTAwMThcXEr/XHUwMDEz+82X4a44Ka6N76PNV8dcdTAwMWYjOJhcdTAwMWS92fq0cXK11npT3r9d/bJn381U146i9zBh+yi9/T5O5dCXu5Umlvn9/Fx1MDAwZVgtXHUwMDEyKVx1MDAwZfeU0UZcdTAwMTdcdTAwMGLbMbb/j1x1MDAxMLaghm0hXHUwMDFk7FRija2cmSysW9Rd/lJcdTAwMTgzVWVwqk6l90yypdW9raXjTl/QXCJ0KPVcdTAwMGZpJINcdTAwMGIrW7r4XHUwMDBlXHUwMDA2KKRcdTAwMDSfa1x1MDAxZb+LePSSz3lX+FRcdTAwMTT2XHJcdTAwMTOGY0Gyxlx1MDAxN4Ogd1x1MDAxYlx1MDAwZVx1MDAwN4zOXHUwMDE3iDhe1Fx1MDAxMmGEh35cdTAwMWGBpVx1MDAwYoS0vqeRQ1x1MDAwMVx1MDAxNlx1MDAwNEPobGWgrZagJZtcdTAwMTOgXFz+6MF3I1x1MDAwN21mlvtcdTAwMDVGsk/lXHUwMDBiXHUwMDAys69ajXZcdTAwMDZLPUVwYVx00Vx1MDAxOFx1MDAxMKT81FHuqodcIjhcdTAwMDbGMZitXHUwMDAzniRp7WCPz1h1q9H7zHpcdTAwMDYloNM6gdJcdTAwMDdcdTAwMWHo01x1MDAxZYOjooC1oUFeclx1MDAwNpiU4tcuXVx1MDAxNULYP/rBm73Zi/zv6dJrVHxjXHUwMDBi35BmnZ5AhVT3bk72t1blUUlhfFgtl0qfP5ZnqkImtGDqUVx1MDAwYlx1MDAxNiAozfAxvu8kn9TsaFx1MDAxMCNcdTAwMDLpXHUwMDA0XHUwMDBiXHUwMDE0R8hRRv+wZpZbU96OknWK/OpcdTAwMDNcZuudfjS5ZslqY1xyPq2ncpFza7xcdTAwMDIzvOFLkTBXuW6HPkpcdTAwMTAvgGOvP75T39i5rq2vJPK02Tyrne5v7X51Z5s/OfKWj0rzwJBjny4sXHUwMDE4Ry5/XHUwMDAzle+0kFx1MDAwMet2sEjOXGIj5rjfb9x6rtTkd8o9z81cYizHXHUwMDAyk2w7/Sc6ntY7gSws/liw3v+Pn1x1MDAwM/uyefYubFx1MDAxZdy//Xik11x1MDAwZeMvd9vVm2e7XHUwMDFiw1TOybeFS/Y9yMFcdTAwMDSgXHUwMDEykEv6d7BcInRgWFx1MDAxM1xinlx1MDAwN806ySx0XVx1MDAxNFhnWpA0/43uI6sz/j4z8/YgUlx1MDAxNXpcdTAwMTDQYNCx3lx1MDAxON+FvK9cdTAwMWbbo1J8vH5v65XD083W12azseguhFx1MDAwMoaLUuyzfVecXHUwMDEzvVx1MDAxYlZcYjj08Dd11PyDr/rpXHUwMDBlRFx1MDAxOVL+plGz24Y20oFImKhE+I9cdTAwMDOZ2oFcdTAwMTTfY1x1MDAxNaThZfftx2MzcVVUrrd0RUNyrfZKXHUwMDFm4q9n9rqojPIsLuRRXHUwMDFh+lx1MDAxNFxmXGJGtkO/P6xv31x1MDAxOElcdTAwMGWjjXaC40q0+Z7kXHUwMDA19Fx1MDAxZlx1MDAxY4uSr3z9XFz34UvN83dcdTAwMWaFXHJhKCx6XHUwMDFkYMbXPXftjav7hjsyNqkmZXO6oe7fvVl098FoIHKotZ9vwVx1MDAxZaJcdTAwMWa4Olx1MDAxMP5mR75Q5XdcdTAwMGL99Po+XG7lt1SoXHUwMDE5llx1MDAwN0Z6XHUwMDEwjrqeelOQ/ztcdTAwMGby4iG5sFx1MDAxYzabXHUwMDA3Kc9o10LwWsXlh2nJPmf5Oo5u1obdWa/z8O/aobbnUNQxN99efPsvXHUwMDFht7uQIn0= - widget.render_line(y=0)widget.render_line(y=1)widget.render_line(y=2)Strip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])Line API WidgetStrip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...]) + widget.render_line(y=0)widget.render_line(y=1)widget.render_line(y=2)Strip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])Line API WidgetStrip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...]) diff --git a/docs/images/scroll_view.excalidraw.svg b/docs/images/scroll_view.excalidraw.svg new file mode 100644 index 000000000..4ca739d02 --- /dev/null +++ b/docs/images/scroll_view.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1dZ3NcIsmy/b6/YmLe16VvuSyzXHUwMDExN17IIYOQR4ZcdTAwMTc3XHUwMDE0mEa01Fx1MDAxOEFjpFx1MDAxYvvfXybSisY0gpFG04MmYmZEY4quU5nnZGVm/fePb9++R09t//tf3777w0opXGaqndLg+5/0eN/vdINWXHUwMDEzL4nR791Wr1NcdTAwMTk9s1x1MDAxZUXt7l//+lej1Hnwo3ZYqvheP+j2SmE36lWDlldpNf5cdTAwMTVEfqP7v/T3Uanh/7vdalSjjjf+kIxfXHKiVufls/zQb/jNqIvv/n/4+7dv/1x1MDAxZP1cdTAwMWRcdTAwMWJdx69EpeZd6I9eMLo0XHUwMDFloOZ8+tGjVnM0WOCWW1xyIN+eXHUwMDEwdLfx41wiv4pXazhkf3yFXHUwMDFl+n5+WONN/354WtnMdC7c/Vx1MDAxZbRvd8efWlx1MDAwYsLwPHpcbl/uRKlS73ViY+pGndaDf1x1MDAxNVSjOl7nU4+/va7bwpswflWn1burN/1ud+I1rXapXHUwMDEyRE/4mGRvXHUwMDBmvtyDv76NXHUwMDFmXHUwMDE54m9cdTAwMTkrPFx1MDAxMJw7Lq1QUrxdpFcraz1cdTAwMDXGSCaFYFx1MDAwZbicXHUwMDFh1lYrxInAYf1cdTAwMGZcdTAwMWL9jFx1MDAwN1YuVVx1MDAxZe5wdM3q+DlC2LKvx89cdTAwMTm8flmtPaaMXHUwMDAxJVx1MDAxNFjNnHp7Rt1cdTAwMGbu6lx1MDAxMT7FXGLPStBMayaYtXY8zq4/mlx1MDAwZsGlUnjdmbcr9PHt/epcYlx1MDAxYf+J37Fm9fWONXthOFx1MDAxZTFd2InBafyaXrtaepl2rnGYSmuQTrm362HQfJh+u7BVeVx1MDAxOCNl9Ojff/5cdTAwMDBCjbVJXGLlWnFcdTAwMDZcXDOzNES3XHUwMDBitYf74cbRY15enZ7XLvrFwvZGyiFcbtrjoIyWSlx1MDAxOIm3w+lZkErpQHLEqTRcXKdcdTAwMTakOHzlXHUwMDFj07BmXHUwMDE41UYkYdRcdTAwMTlhnDGCL1xyUf9i71x1MDAwMO9euVxcVWcn+X2by508qJRDNMOlx7iz4LhBp2Etn8QoaOkpbVx1MDAxOV7iwqrpcaVcdTAwMDehXHUwMDFjpLT4h69cdTAwMWJCtUtCKGdcbn1cdTAwMDc6uOVcdTAwMWT99XH5KHdcdTAwMDO23Do5bZSu+3vIb7q/XHUwMDE2opy9a0aVZ1x1MDAwMPmMsJo79Fx1MDAxY2ZcdTAwMDKhWjpcdTAwMGZJgHNooVx1MDAxOChcdTAwMTCphajgxihcdTAwMWPq2kHUJlNRgU7PxrnNe1xibWSKJZmtbFx1MDAxNM6L9Vx1MDAwN3+7frKzXHUwMDFi8rRcdTAwMWJRyz1kocpKLSyuypiZXHUwMDFjIVQxXHUwMDBmLahcdTAwMDLFXHUwMDE1LldcdTAwMDOpRSg3OF2APmDtuCgkc1GLRlx1MDAwM+9cdTAwMTd3S2PUmZKGsHV/36tcdTAwMTT4bdirto7NUcoxylx1MDAwNXhOI+k2XHUwMDFjIeBigujFzVx1MDAwM1JR5ONWSMVkbP5TXHUwMDA30dFrOfKVr4IosK+BqJGJXHUwMDEwXHUwMDA1po1hsFx1MDAwMlx1MDAxN+08XHUwMDFm1DfMzlx1MDAxMfC9Pelntp+zJ/XLtDt6nFv09MjhjFx1MDAwMPxcdTAwMTfspKhH5HrCopbXWignRJrJKLPcXHUwMDFhxtZcdTAwMGakOlx1MDAxMaROc1x1MDAwNcwsb0Y3ZNlFjf1n26rvne9cdTAwMTSueOmymXZJn3FcdTAwMWVjXHUwMDEylyMuRvT4ztgpiIInXHUwMDA1zoZRgOIxvVx1MDAwMOVGo6bH91kzgGrDklx1MDAwMCpcdTAwMTjn3FomYGmEbjrVclH0sFnuPoiGrvLuTbOdcoRyXHUwMDA3XHUwMDFlV4YzriRcdTAwMDXZxKSnV1YjNlx1MDAxOP7DtEB/z1V6QSpcdTAwMTE/1uCr11xmpKhcdTAwMTSSQGq0tMahpl1cdTAwMWGjuXBY367LYj5ifrijXHUwMDA3T2F7eJFyjEopPWSaXGbQkTOrxXTwXHUwMDFlIYomXHUwMDE2QWGYxGWb3rgorjFkI1xm5JdcdTAwMDVGv8rRi0RH76RDcsPY8lx1MDAxMIWHnbBcXK31s+d9YNXidXjb3Fx0U1x1MDAwZVGhlYdLXHUwMDExXHUwMDE3I1d431x1MDAxOUxAXHUwMDE0NPcsQldcdFxuS4lcdTAwMTTbUNCGXHUwMDBinK/1XHUwMDAzaGLkXmryb2pcdTAwMDVcdTAwMWK69aBuXCJ+X70rXHUwMDFjVTqXg44s1EultKslMqJKOe0kXCKAczbJRCnoxIW2hvZGudUphqiRktNfa1x1MDAwN9HY9ua0WFx1MDAxMjhcdTAwMTSJXHUwMDBic1x1MDAwNSN6f7N3cGhcbsdcdTAwMWLKb4mD48vLdtGkXHUwMDFkoyhcbj1cdTAwMDSHldohIZVyXHUwMDA2pMJDrYRcdTAwMDRAoHKKsaLUYVx1MDAxNFx1MDAxN5lFovJ1m/RfRUV5sqBcdTAwMDenXHUwMDE0V9YsXHUwMDFmdVx1MDAxMntcdTAwMWLmLDxcYor9rfb93bFo1Nh9MeWOXjHpaYG60DnlOOr2KUePNEAgNJi0TqCtTS9ENVijXGaYdTOjVibugKJcdTAwMGI0xki9vFx1MDAxNT2KbvuBelx1MDAxOF62N1x1MDAxZotcdTAwMWLRU/78rL7zXHUwMDFiIFRcdTAwMDGnfCZAPynkpKJHfuchbmn6LaN8k9RClFJhjOFq7Vx1MDAxMMpcdTAwMTK5KJdcbmjThS2/R39zONgt14e1sGev9pvuaqvVPL5Ju6NHpulpY1x1MDAwNSolri1cYmWmMGo8JizOXGaCh1x1MDAxMolSi1HBnEQzis5vzUBcdTAwMWGH4HRklMLBpGSXt6PhzXPxWFfgtKFcdTAwMDfN6DjXuO9cXJ+m3I5mLEpcIs0kt1JSzGZcbqKOeYZrNLS4XFyFVelcdTAwMTVMXG6kcFoquWZcYrWgklx1MDAxMMqtMEY5sULC6N2gkr1cZrrc3lx1MDAxNp5cdTAwMGWCutLnxZ1fXHUwMDFjXHUwMDE3XVwi1cmgXHUwMDFlkkjKXHUwMDExg1bZOD5eMMqRXHRoh1PDUfHHbFTaMIpcdTAwMTetlcqu3Vx1MDAwNmgyRonbaK2X313yuzVVubjIQa691dbFfK++U95Lu1xy5cxDpums5lx1MDAwMijhLuYz6FxyLIJcdTAwMDdlXHUwMDE0in2UzIid9JJRq61hbt2oqLHJWSRcdTAwMDKnXHUwMDA0OZhcXF7PO1x1MDAxNtajXnnX+IfVgOWvK73MYSvlXGJFXHUwMDEzioJQOEAuXG4uvttLL3eKeU5cdFxcq0owmeJcdTAwMTRcdTAwMTLU8UhF5PrF7WPhpFx1MDAxOVx1MDAxYSosY1qsYELLrTq/61x1MDAxY+RcdTAwMWHPmyeNu3774Ph6J59ygGaM9Vxmulxu9N9cdTAwMTaZJlx1MDAxYdIphEqPNL41XHUwMDAynLZcIr3ZooIhmWaCs7UjojJxf14xYFayXHUwMDE1bOh5/iZz8XB9tN+v3PT2j/1wMDjup1x1MDAxY6JcXKBcdTAwMTFF52iYQHzqqc1P9JyolFDpo1x1MDAxMFHO8PRm3Fx1MDAxYmHAraFcdTAwMTGNf9Xp3Xmh0LdJvTxAd/bt4O4y18z3/dPCxuZF9bbXTHtIlFx1MDAwMEqln1x1MDAwZVxykKSakFx0gFwi+1x1MDAwNI9JQCNcdTAwMGKgdDxJL21cYuWa/oBbPzGvXHUwMDEziSg4UFx1MDAxY9n30lxi7e9cdTAwMGWi67P9u/x20d86uL85qNRL1ylHaMZ4XG59o0J3XHUwMDAxWlx1MDAxYjeVy4xcdTAwMTA1nlX4w5DpoZZKr1x1MDAxMUX7L1xmY2uXhWdE4r6SZVKD4CtU0LvDXHUwMDA33ogye4ftx5NwN5dpXHUwMDBl4fYk7dEm7lxmldAz9PJcYtU463lcdDZcdE9xXHUwMDE41dyNNjHSi1COa4ip9YuIuuRcZlx1MDAxMqesXHUwMDAy59zyfr5cdTAwMTjUud/KZINmvlx1MDAxMEB+b7flXHUwMDA2v1jML5PkpD1cdTAwMDQgUVH09256W1x0ISpcdTAwMWS3XHUwMDA0XHUwMDFlQbs2qYWo1lx1MDAwMlDR2bUzoi7RiDpcdTAwMTRKkmoklkbo9eVppeVOq3dXwVnhXHUwMDAyoPPc2bxKuZ9cdTAwMTeadjZRryuFpFx1MDAxNMnmZMTe4mWFQlx1MDAxZeW+XHUwMDA0kWZcdTAwMWIqqMKMXGLzmiE03lx1MDAxMmCmgFx1MDAxZTUuuj29vJ8vXFxcdTAwMWa0d4P93bN8Z/9cdTAwMTAuovOry27aO+VIXHSe5VxmaSgyUmXdpFhyinuWMkuEJFef4pAol5Kj29Nf14Xk1yfhXHUwMDE5So7UdoVke7d53bzs12+jsti0e7Xn8rY/eEw5QrmzXHUwMDFlSnnDnFx1MDAwM1xynEs9XHUwMDA1UeWh+1x1MDAxNHgrXHUwMDE4l0KmV847XFxnhpm1I6JxozFTVudw3lx1MDAxONjljejJUc70MvKKXzzdN1x1MDAwNyYrovKen3KIKobwQCGM8yuYxEmejok6T1x1MDAwM4p6pKia9kbTXHUwMDBiUdBKOFxy65bJ7JLTRFFcdTAwMWRKSuJd3ojulMNm5/G4nLX8YPPsYfdhsFx1MDAxZJ6lXHUwMDFloeTmkYkyg2aIs0lcdTAwMWI6ioniSkXzqpyWMr2pI1xc4PJB7bB2RtQlt2xE5uVor2V5XHUwMDFium1y/WKws/1cdTAwMWPehe2gye3u3W4j5VxiRevpMdTrXHUwMDAyZ1x1MDAxZt04s2ZcdTAwMWGitDPqJFx1MDAwN8DZifO81GFUUXWqiG+zrFx1MDAwN0alTNxZQu5FU6NWXHUwMDEw9PV+aT97aMPhTXk7a1x1MDAwZUr2LOcnZThNgW1cdTAwMTKi08mYb6+qlrp1f1x1MDAwNYyad+P2VlDASYAyXHUwMDE0d5qO21NtsuZUUmlcdTAwMDDExC7GXHUwMDBmYDTqlJrddqmDXHUwMDE4mMUpgPK0Nlx1MDAxNlxmt05CrI7/XHKniFBv1FFcblx1MDAwMMkzn9dyjOrMjFCfh9PXXHUwMDBiY2DF5rtlbvaOb8RG+/K8e1lcdTAwMGI6rqnz4+ZdXHUwMDEzKCx1Oq3B97crf/+56H3z14f3rHSlj+rnXHUwMDE1dtIuR2FcdTAwMGX2l3vf1/8lry7K+bWgv2R1cctcdTAwMTLTXHUwMDA3jbC0YblCM7+N21opujs5Pm9l6rJcXMmen++qpFjEj62un9HKT2lcdTAwMGKgrUKVXHUwMDA3bqpcdTAwMWNQWk8qLt2ojilcdTAwMWWK+lx1MDAxMftfq9VmXHUwMDE3lVTWXHUwMDAzyv1cdTAwMTIkMy23s4tcblx1MDAxZJPnqJBcdTAwMDG1psZJmVx1MDAxM4dwgkpuYd2Mv1vU3cdYUt92+dwsU9nLNSvlKHrK9a3eeK5cdTAwMWWq3jDt+1xyXHUwMDAwnpTURVx1MDAxMuGnQGkzyaK14p401F9cdTAwMWJcdOpEzXXaXHUwMDE4iuFSMbV2m7bOJkJU472a7F34XHUwMDFlQvc3XG79QfhQUMWbw+3sZkFtmrNK2lx1MDAxMcqd8lx1MDAwNMp3SX0nXHUwMDE4XHUwMDE3alx1MDAwNqCMa8GQqVHBdZopNCVHOFg7XG7tYjd9JvNFkFx1MDAwNOfLQ9Re7uYvh49mb3PrPNdX16Hc2vvFPdKW2bRcdTAwMTWeQdvjkFx1MDAxZjvjzHTHXiU8SdWA1qF3TXPzXHUwMDE0R1vs6Fx1MDAwNNbNivJ4i9rpbVvNrY1ff1x1MDAwZqLDp42HsFx1MDAxNO5cdTAwMWNcdTAwMTTPhsXipSuHh7bzIzR0Olx1MDAwNeqn0lDG8J4jXHUwMDAzdEDJ1lNeXHUwMDFleSjScYNcdTAwMDDVlF3wM2io8UDhXHUwMDE4JHVoXHUwMDAzNUfbceLCklx1MDAwYomyXHUwMDBlZSaPl9K85lx1MDAxNFxiy6RUn7hcdTAwMWT2emGuXHUwMDA2XHUwMDEzYviU63b5+UA1+cHTyYE7XHUwMDBmup+g7VrX1cst8Vh7PKpcdTAwMWY7rVub+iR/8WnaTlxuiDVcbvrgmvLDMGh3568oyZM7XGYr5yxzq2RCbGX2nvfPj1xucnhcIrv3xXZ2cFx1MDAxYyZF91x1MDAxNq6przP6XHUwMDFhiVx1MDAwN51xXHUwMDAx1Pg03lbwxeRcdTAwMDPSXHUwMDEyVE2oqsxcdTAwMDerv2slwEmdXVMk7HDJ4utcdTAwMTUyo/hpXHUwMDFhb2vKok9cdTAwMTBmVDw56icvp5dcdTAwMTR3Qo1cdTAwMWF7ft7uyE9aU0tgXzmIybWfif3kM1xuwFx1MDAwMUi1SmD7iVx1MDAwNTf1/oHhfJBcdTAwMWLWXFwh2+tdi3RD36CppqpxjdCmcpjp9u9I2KnXoXVcdTAwMTLR9cFk9FqpjG7j56Cfmk9q+Mxg4ZfRnUXwVFwiMVx1MDAxNVx1MDAxZIU8MvJ4R5J3W8JcdTAwMGVPLs+u77rNfKd5oSHT2Fx1MDAwN5tNNzyd9tBgSsGMY8qJKXBKz+JcdTAwMDLmhrLUwHysv1x1MDAxMVx1MDAxN2Vr55Dxz1x1MDAwMKdQynKxhkG35NMzuFx1MDAwNGXUSlx1MDAxNbuP+9lgj4iDuXlu9jbEw0Bmf3FcdTAwMDeuJei4RlxmOjlSjFx1MDAwMPFzw0ZbNnRMXHUwMDE2JZOiiEO3luL+RpxcdTAwMGJNmzFr1+Ao3rBnZldcdTAwMTBHwrhZoVZcImtcdTAwMWVcdTAwMDPjXHUwMDBlaq2to63raCNo+LntX9xVe7mYhpNcdTAwMDBcdTAwMTZ1I0V+J308NY+hXCJcdTAwMWXUaZRcdTAwMGKV5iRKXHUwMDFjnTSW/Y5OfnHnd5VYK2GFcJrqXHUwMDA3loZo4TTfk4em8LjNdjZzuX5hUyRWnKXEy4PxkFuilURHayF++ucoP1xyXHUwMDA0XHUwMDFhUW2onSOdifkhfFb8qqqWZvEpkelKw6TRQlx1MDAxYlxcLLG43zisQf1cdTAwMTStQ7pB4Vx1MDAxNdBaTyN0XHUwMDE09TCSr1vQzSVHXGJIJFlcdKvQUD9T4mf3NmpfPcnNnWHtML+dej+PPt4zQlx1MDAwM50wRKdoiNnuRoxpOslNKzRT6W1cdTAwMTJnR1lcdTAwMTl67WwofudELsrwnjnrlofoTdjdkNnh9uVcciuUjrWJStlccpluXHUwMDFiyqX1kMFcdTAwMTmJ5lx1MDAxMlx1MDAxOY2FqSRfQLFkUU4yZpHruY95eVXRfm2OkueS46eAXHUwMDExTvBRavWcXHUwMDFjXHUwMDA1OrVTMFxyXHUwMDE2mLPU3l1NQ1TTQYlcdTAwMDBfd9jgV1lRkSiXlFx1MDAxMeji2FxuPbVvXHUwMDFlh0enN1x1MDAxYqWDKLeZk1d3bGuHp39cdTAwMDNYSI+yeYGSza1cdTAwMDA23WpcdTAwMTM8bZWUXHUwMDEyvT3EXHUwMDBmvkubXHUwMDE15Vx1MDAxNI8wZv16XHUwMDFmOJG8XHUwMDE3XHUwMDAwVlx1MDAxMEWC5UFcdTAwMWFtXHUwMDA1R/WgWzdcdTAwMWI1lW1l7lx1MDAwM3Ppl9NcdTAwMGVS9PJUsoN+XFxcdTAwMWGlXHUwMDEwitMgdVx1MDAxZdovxCqFdOJN5NJcdTAwMDZSnCluKVx1MDAxZnTNMFx1MDAxYT9cdTAwMTFipnCXXHUwMDBiZsRcbudk1bcv3P5Teeu2pE6e29cl/7Rym3TydUpcXL202lx1MDAxM1x1MDAwMtmopSTe+Fx0taOwKGhUMlx1MDAwZUWIZFoy9zF8Jukl9OKKsk81UJqhdrGaorGnd4rO6zLcKK6EZmZ2z1xuIU69XHUwMDBm1q9jMSzYVjKGsVWOeLnh2UplWDhcdTAwMGVOs+y+556zXHUwMDE3YifpQMzUQFx1MDAxNI0kbeRcdTAwMDFyUif1JFx1MDAxOdWkllx1MDAxMKD2RTzqj0Xuk8go8mHlLNVIcyRXLFZcdTAwMWQwRihjnjI4PIaTgsYyXHUwMDE2vv2nlyGaeerR8Fx1MDAxYpacRf4wmodNnlxcVa6RkFx1MDAxYlx1MDAwMSvkIZ5W84/Hd2ew275cdTAwMWJcZlx1MDAxYvno3u+ap3SXSSiH2ETLiV7CUidcdTAwMTI9mUFjXHUwMDE0XHUwMDE19DKgvIdcdTAwMGZcdTAwMWYnPDeDRsBcdTAwMWNvLmeCSWhTXHUwMDExXHUwMDBln1x1MDAxOUx6vfCl9Vx1MDAwZpfZU13JVEphsSOHw61iodhcdTAwMTPnn5QnoLWlLmorLJgxMFvN6Dx4XHUwMDFlbVx1MDAwM9iJR7OlRlx1MDAxMD5NgGu0knCA/aBcdTAwMTP1SuFtXHUwMDE3X+i9zlxcbPq7Plx1MDAwZWD0jjDx0o0wuKOF9z30a5MrMlxu8Ma8XY5asa2EXG5cdTAwMGWlhG/X2a9Of6VWJ7hcdTAwMGKapfBiwbBcdTAwMTZahpdcdTAwMWI9xzTIZIVcbijKhNErdDFfjKhfkFxc965lQD3qIaqUQJ+BtMRNXHUwMDE1+Vx1MDAxOVx0XHUwMDFlnYdhkfcjq5FcdTAwMWZzW/OT69AvXG5UlYBcIphROs+c7Wb0q55DgkFcdHbKonebsVx1MDAxY+SxjLGWfYXXWr1cYil220qdaDNoVoPmXHUwMDFkXlx1MDAxY1uP7/7LR+8v4WPoy5XaL5SUXCLRXHUwMDFj74xDT6rHZ3KNVnalR18kwzzkxXRwl5OG+KtcdTAwMTWvT3ozY9/9ZvX9QS2uXHUwMDBmfFx1MDAxYpTknlPc4mxcdTAwMDCXbsxB4kPiXHUwMDFl51Kh1lx1MDAwNIlUmrJtZoZcdTAwMTSWutFWq9FcYlwivPUnraBcdTAwMTlN3+LRvdyghV33SzMmXHUwMDAzv1L82rRcdTAwMDVo0ztOuojx/76NV8nol7f//+fPuc/OJIN4dHlcdTAwMTa/43f8I/7vyuaLJ1x1MDAxZk5Le/Ocs1x1MDAxNWLAi1x1MDAxZFc6zZfzUPMpyv3WUnOY2qOw1mPMKGGRknNmf0KJXHUwMDFh81x1MDAwMFmToNguoLDjZk5ysGTCXHUwMDEzmo7w1pyaUdhZ88XpQHYrxdeR7vH9/lx1MDAwNebrXHUwMDFmQ8A8a1x1MDAxZLlZh8JeUZnujDlBTcM0amqUKiSqtVtsvCa/xW9kQ5KBRD+zXHUwMDEwWtGCJEmjeC/TmYQmXHUwMDAzgKZ8XHUwMDA1bdQqPF01TMtvV/LX3WM1dJC5/KGd+K/TRijbjUPNjrLIWlTHkzvxVDXoKapcdTAwMWWUqKul+mDsc744UkuJI0pVNWA/8Vx1MDAxMIw3XGJ9blx1MDAxMfdCcXT7iFx1MDAxNL+xVb3bvkV/uXdxcrxfqX+aOHLUzunXiKOX2UybNnpcdTAwMTnVj3FcdTAwMGJcdTAwMDHJXHUwMDA1xki0KTNpeW6xXHUwMDE4T2nkXHUwMDE2UiGro+OaKVdcdTAwMWOMXHUwMDEw05ZBXGLPoltS1OROfvTwnLmWgTPt0d4yMmlEuFJsjqHgnkZSqdExUltL62LBk3+iKsZcdTAwMWEmv2Lb7udcdTAwMGKjxVx1MDAwZeaNNDhPOGOENnzUko/NXHUwMDE1IcxjXHUwMDE0XHKj7Fx1MDAxNkZpoOh0XHUwMDE3c4tcdTAwMGZcbiNhPY5uxpGfMZKOvJkvjZhjtNGArFxih6V/c2mUXGZh+snMondFXpOsjExihqFcdTAwMDGUwtbEsi3es16LvVZcdTAwMWGtl1FcdTAwMWX1t2NcdTAwMDJcdTAwMTSDiVx1MDAwNnKvxst4xjJcdFrhXHUwMDE34vDBXCKvucZcdTAwMGJQ5VtNMLa4xOLNKOLGyzBljdOSmjHiQGaMXHUwMDE3woK7rzon+Yf5w2eZr5hxoiOEXHUwMDA1MlPpmKXOXHUwMDAx8djOa/hcdTAwMDfoaFwiOnmIW8Glcu/Yr99WXHUwMDFiZZKx9HJ5XHUwMDA2RitakWR1lGhEqK1cYt56I5a3XCIn9d3BMCzmn56et0Bu5Fx1MDAxNTRr27/Sirh3xZFHLcQsXHUwMDFk0IbSR05ZXHUwMDExoM5cdTAwMDHGOIfYQ1x1MDAxMfNTjMg8qzFHXHUwMDFijVx1MDAwZWVxnymOXHUwMDE27sHgLVx1MDAxOfOpny0zupVOK1xmb1u1WtdPxe7L5IB+zD1Llth3QzCEnEKvtHyRyuLS3DT654xcdTAwMDGPS+CS0qe51GoyX0A5Ntqx1VYoZDL8J0QuueCe0tRXXHUwMDAx7eaoXHUwMDFi7lx1MDAxY/+spWfokGVj0Fx0aTnbXkvQSVx1MDAxZUZ9zYEzeFx1MDAxZoT+sd3PKVx1MDAwNz3tyt533YuL/OOuW1ErXGIrkF+imcDbXHUwMDEzXHUwMDE3XHUwMDFmb45bOmqpwoxcdTAwMDQpUVxm/N5cdTAwMWM/XHUwMDExSaOrM1x1MDAxOPok56yTc4uVo0TCXHUwMDE1OvecXT1cdTAwMWM37/qXwX3vJl9cdTAwMDJ5UeVcdTAwMDdhql1zhjvnOS6cpoP90PPZ6Vph56HWZNbRXHSq8Yq/T299OVx1MDAxZf9cIlx1MDAxN40q0Fx1MDAxOfeZOe5vOEp9o5KPXHUwMDExgKd/szS4fVx1MDAxYcaPOfvkNnCoPo0wYoVcdTAwMWSGhfOazoUqXHUwMDE1ddDiXHUwMDFjkKFS+e1kIFx1MDAxMVWnZ9FowqhcdTAwMTbQsVx1MDAwZlX7LVxcqNyiqmR07rmQjDNcdTAwMDVz8lx1MDAwM9Uoo4DqpTRSMJjTT9MwwVx1MDAxY1UnfonD/+GVtqRcIl9s+L/Ftyq5odwrTcE7bpRSc/y68jRnXHUwMDE2hTgqVVx0+m2XbsWA4uImXd8mczuorVx1MDAxNEVcdTAwMTU5XHUwMDE31up5XHUwMDFiqFx1MDAxMlx1MDAxN1x1MDAxOaP+fdRa38rfm2skYph+MjPwTeJcdTAwMWF/vH7C91K7fVx1MDAxZSHo3qZcdTAwMDPhXHUwMDFjVF9t9Phrfu9cdTAwMDf+YHM+tyZ6/cfrXHIlXHUwMDBi5NOX/e/ff/z9/96SXHUwMDBlNiJ9 + + + + virtual_size.heightvirtual_size.widthscroll_offsety=0 diff --git a/mkdocs.yml b/mkdocs.yml index bbb962580..ad2d90d47 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -167,6 +167,7 @@ nav: - "api/query.md" - "api/reactive.md" - "api/screen.md" + - "api/scroll_view.md" - "api/static.md" - "api/strip.md" - "api/text_log.md" From 25b498896dc583570fccbcef43d220a1bb2e7b3c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 4 Feb 2023 11:28:07 +0100 Subject: [PATCH 06/17] checker 4 and diagram --- docs/examples/guide/widgets/checker04.py | 108 +++++++++++++++++++++++ docs/guide/widgets.md | 37 ++++---- docs/images/scroll_view.excalidraw.svg | 6 +- 3 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 docs/examples/guide/widgets/checker04.py diff --git a/docs/examples/guide/widgets/checker04.py b/docs/examples/guide/widgets/checker04.py new file mode 100644 index 000000000..54f2d7522 --- /dev/null +++ b/docs/examples/guide/widgets/checker04.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from textual import events +from textual.app import App, ComposeResult +from textual.geometry import Offset, Region, Size +from textual.reactive import var +from textual.strip import Strip +from textual.scroll_view import ScrollView + +from rich.segment import Segment +from rich.style import Style + + +class CheckerBoard(ScrollView): + COMPONENT_CLASSES = { + "checkerboard--white-square", + "checkerboard--black-square", + "checkerboard--cursor-square", + } + + DEFAULT_CSS = """ + CheckerBoard > .checkerboard--white-square { + background: #A5BAC9; + } + CheckerBoard > .checkerboard--black-square { + background: #004578; + } + CheckerBoard > .checkerboard--cursor-square { + background: darkred + } + """ + + cursor_square: var[Offset | None] = var(None) + + def __init__(self, board_size: int) -> None: + super().__init__() + self.board_size = board_size + # Each square is 4 rows and 8 columns + self.virtual_size = Size(board_size * 8, board_size * 4) + + def on_mouse_move(self, event: events.MouseMove) -> None: + """Called when the user moves the mouse over the widget.""" + mouse_position = event.offset + self.scroll_offset + self.cursor_square = Offset(mouse_position.x // 8, mouse_position.y // 4) + + def watch_cursor_square( + self, previous_square: Offset | None, cursor_square: Offset | None + ) -> None: + """Called when the cursor square changes.""" + + def get_square_region(square_offset: Offset) -> Region: + """Get region relative to widget from square coordinate.""" + x, y = square_offset + region = Region(x * 8, y * 4, 8, 4) + # Move the region in to the widgets frame of reference + region = region.translate(-self.scroll_offset) + return region + + # Refresh the previous cursor square + if previous_square is not None: + self.refresh(get_square_region(previous_square)) + + # Refresh the new cursor square + if cursor_square is not None: + self.refresh(get_square_region(cursor_square)) + + def render_line(self, y: int) -> Strip: + """Render a line of the widget. y is relative to the top of the widget.""" + + scroll_x, scroll_y = self.scroll_offset # The current scroll position + y += scroll_y # The line at the top of the widget is now `scroll_y`, not zero! + row_index = y // 4 # four lines per row + + white = self.get_component_rich_style("checkerboard--white-square") + black = self.get_component_rich_style("checkerboard--black-square") + cursor = self.get_component_rich_style("checkerboard--cursor-square") + + if row_index >= self.board_size: + return Strip.blank(self.size.width) + + is_odd = row_index % 2 + + def get_square_style(column: int, row: int) -> Style: + """Get the cursor style at the given position on the checkerboard.""" + if self.cursor_square == Offset(column, row): + square_style = cursor + else: + square_style = black if (column + is_odd) % 2 else white + return square_style + + segments = [ + Segment(" " * 8, get_square_style(column, row_index)) + for column in range(self.board_size) + ] + strip = Strip(segments, self.board_size * 8) + # Crop the strip so that is covers the visible area + strip = strip.crop(scroll_x, scroll_x + self.size.width) + return strip + + +class BoardApp(App): + def compose(self) -> ComposeResult: + yield CheckerBoard(100) + + +if __name__ == "__main__": + app = BoardApp() + app.run() diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 80676284c..96b9cbff1 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -206,7 +206,7 @@ Textual offers an alternative API which reduces the amount of work required refr !!! note - The Line API requires a little more work that typical Rich renderables, but can produce very power widgets such as the builtin [DataTable](./../widgets/data_table.md) which can handle thousands or even millions of rows. + The Line API requires a little more work that typical Rich renderables, but can produce powerful widgets such as the builtin [DataTable](./../widgets/data_table.md) which can handle thousands or even millions of rows. ### Render Line method @@ -251,11 +251,11 @@ This would create the following object: --8<-- "docs/images/segment.excalidraw.svg" -Both Rich and Textual work with segments to generate content. When you run a Textual app you are seeing hundreds or perhaps thousands of segments combined together. +Both Rich and Textual work with segments to generate content. A Textual app is the result of combining hundreds, or perhaps thousands, of segments, #### Strips -A [Strip][textual.strip.Strip] is a container for a number of segments covering a single *line* (or row) in the Widget. A Strip will at least one segment, but often many more. +A [Strip][textual.strip.Strip] is a container for a number of segments covering a single *line* (or row) in the Widget. A Strip will contain at least one segment, but often many more. A `Strip` is constructed from a list of Segment objects. Here's now you might construct a strip that displays the text "Hello, World!", but with the second word in bold: @@ -268,15 +268,15 @@ segments = [ strip = Strip(segments) ``` -The first and third strip omit a style, which results in the widgets default style being used. The second segment has a style object which applies bold to the text "World". If this were part of a Strip it would produce the text: Hello, **World**! +The first and third Segment omit a style, which results in the widgets default style being used. The second segment has a style object which applies bold to the text "World". If this were part of a widget it would produce the text: Hello, **World**! -The `Strip` constructor has an optional second parameter, which should be the *cell length* of the strip. In the code above, the length of the strip is 13, so we could have constructed it like this: +The `Strip` constructor has an optional second parameter, which should be the *cell length* of the strip. The strip above has a length of 13, so we could have constructed it like this: ```python strip = Strip(segments, 13) ``` -Note that the cell length parameter is _not_ the total number of characters in the string. It is the number of terminal "cells". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to leave the length parameter blank. +Note that the cell length parameter is _not_ the total number of characters in the string. It is the number of terminal "cells". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to leave the length parameter blank so that Textual calculates it automatically. ### Component classes @@ -297,24 +297,23 @@ The following example replaces our hard-coded colors with component classes. ```{.textual path="docs/examples/guide/widgets/checker02.py"} ``` -The `COMPONENT_CLASSES` class variable above adds two class names: `checkerboard--white-square` and `checkerboard--black-square`. These are set in the `DEFAULT_CSS` but can modified int he apps `CSS` class variable or external CSS. +The `COMPONENT_CLASSES` class variable above adds two class names: `checkerboard--white-square` and `checkerboard--black-square`. These are set in the `DEFAULT_CSS` but can modified in the app's `CSS` class variable or external CSS. !!! tip Component classes typically begin with the name of the widget followed by *two* hyphens. This is a convention to avoid potential name clashes. -The `render_line` method calls [get_component_rich_style][textual.widget.Widget.get_component_rich_style] to get `Style` object from the CSS, which we apply to the segments to create a more colorful looking checkerboard. +The `render_line` method calls [get_component_rich_style][textual.widget.Widget.get_component_rich_style] to get `Style` objects from the CSS, which we apply to the segments to create a more colorful looking checkerboard. ### Scrolling -Line API widgets require a little more more work to handle scrolling. +A Line API widget can be made to scroll by extending the [ScrollView][textual.scroll_view.ScrollView] class (rather than `Widget`). +The `ScrollView` class will do most of the work, but we will need to manage the following details: -A Line API widget can be made to scroll by extending the [ScrollView][textual.scroll_view.ScrollView] class. We also need to manage the following details: +1. The ScrollView class requires a *virtual size* which is the size of the scrollable content, and should be set via the `virtual_size` property. If this is larger than the widget then Textual will add scrollbars. +2. We need to update the `render_line` method to generate strips according to the current position of the scrollbars. -1. The ScrollView requires a *virtual size* which is the size of the scrollable content, and should be set via the `virtual_size` property. If this is larger than the widget then Textual will add scrollbars. -2. We need to update the `render_line` method to compensate for the current position of the scrollbars. - -Lets add scrolling to our checkerboard example. A standard 8 x 8 board isn't really sufficient to demonstrate scrolling so we will also make the size of the board configurable, and set it to 100 x 100, for a total of 10,000 squares. +Lets add scrolling to our checkerboard example. A standard 8 x 8 board isn't really sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares. === "checker03.py" @@ -327,9 +326,15 @@ Lets add scrolling to our checkerboard example. A standard 8 x 8 board isn't rea ```{.textual path="docs/examples/guide/widgets/checker03.py"} ``` -The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). +The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). You can update the `virtual_size` attribute dynamically as required, but our checkerboard isn't going to change size. -The `render_line` method gets the scroll offset via the `scroll_offset` property. This attribute is an [Offset][textual.geometry.Offset] that indicates the position of the scroll bars. It starts at `(0, 0)` but will change if you move any of the scrollbars. +The `render_line` method gets the *scroll offset* which us an [Offset][textual.geometry.Offset] containing the current position of the scrollbars. We add `scroll_offset.y` to the `y` argument because `y` is relative to the top of the widget, and we need the line relative to the scrollable content. + +We also need to compensate for the position of the horizontal scrollbar. This is done in the call to `strip.crop` which *crops* the strip to the visible area between `scroll_x` and `scroll_x + self.size.width`. + +!!! tip + + [Strip][textual.strip.Strip] objects are immutable, so methods will return a new Strip rather than modifying the original.
--8<-- "docs/images/scroll_view.excalidraw.svg" diff --git a/docs/images/scroll_view.excalidraw.svg b/docs/images/scroll_view.excalidraw.svg index 4ca739d02..0d3ba66a8 100644 --- a/docs/images/scroll_view.excalidraw.svg +++ b/docs/images/scroll_view.excalidraw.svg @@ -1,6 +1,6 @@ - + - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1dZ3NcIsmy/b6/YmLe16VvuSyzXHUwMDExN17IIYOQR4ZcdTAwMTc3XHUwMDE0mEa01Fx1MDAxOEFjpFx1MDAxYvvfXybSisY0gpFG04MmYmZEY4quU5nnZGVm/fePb9++R09t//tf3777w0opXGaqndLg+5/0eN/vdINWXHUwMDEzL4nR791Wr1NcdTAwMTk9s1x1MDAxZUXt7l//+lej1Hnwo3ZYqvheP+j2SmE36lWDlldpNf5cdTAwMTVEfqP7v/T3Uanh/7vdalSjjjf+kIxfXHKiVufls/zQb/jNqIvv/n/4+7dv/1x1MDAxZP1cdTAwMWRcdTAwMWJdx69EpeZd6I9eMLo0XHUwMDFloOZ8+tGjVnM0WOCWW1xyIN+eXHUwMDEwdLfx41wiv4pXazhkf3yFXHUwMDFl+n5+WONN/354WtnMdC7c/Vx1MDAxZbRvd8efWlx1MDAwYsLwPHpcbl/uRKlS73ViY+pGndaDf1x1MDAxNVSjOl7nU4+/va7bwpswflWn1burN/1ud+I1rXapXHUwMDEyRE/4mGRvXHUwMDBmvtyDv76NXHUwMDFmXHUwMDE54m9cdTAwMTkrPFx1MDAxMJw7Lq1QUrxdpFcraz1cdTAwMDXGSCaFYFx1MDAwZbicXHUwMDFh1lYrxInAYf1cdTAwMGZcdTAwMWL9jFx1MDAwN1YuVVx1MDAxZe5wdM3q+DlC2LKvx89cdTAwMTm8flmtPaaMXHUwMDAxJVx1MDAxNFjNnHp7Rt1cdTAwMGbu6lx1MDAxMT7FXGLPStBMayaYtXY8zq4/mlx1MDAwZsGlUnjdmbcr9PHt/epcYlx1MDAxYf+J37Fm9fWONXthOFx1MDAxZTFd2InBafyaXrtaepl2rnGYSmuQTrm362HQfJh+u7BVeVx1MDAxOCNl9Ojff/5cdTAwMDBCjbVJXGLlWnFcdTAwMDZcXDOzNES3XHUwMDBitYf74cbRY15enZ7XLvrFwvZGyiFcbtrjoIyWSlx1MDAxOIm3w+lZkErpQHLEqTRcXKdcdTAwMTakOHzlXHUwMDFj07BmXHUwMDE41UYkYdRcdTAwMTlhnDGCL1xyUf9i71x1MDAwMO9euVxcVWcn+X2by508qJRDNMOlx7iz4LhBp2Etn8QoaOkpbVx1MDAxOV7iwqrpcaVcdTAwMDehXHUwMDFjpLT4h69cdTAwMWJCtUtCKGdcbn1cdTAwMDc6uOVcdTAwMWT99XH5KHdcdTAwMDO23Do5bZSu+3vIb7q/XHUwMDE2opy9a0aVZ1x1MDAwMPmMsJo79Fx1MDAxY2ZcdTAwMDKhWjpcdTAwMGZJgHNooVx1MDAxOChcdTAwMTCphajgxihcdTAwMWPq2kHUJlNRgU7PxrnNe1xibWSKJZmtbFx1MDAxNM6L9Vx1MDAwN3+7frKzXHUwMDFi8rRcdTAwMWJRyz1kocpKLSyuypiZXHUwMDFjIVQxXHUwMDBmLahcdTAwMDLFXHUwMDE1LldcdTAwMDOpRSg3OF2APmDtuCgkc1GLRlx1MDAwM+9cdTAwMTd3S2PUmZKGsHV/36tcdTAwMTT4bdirto7NUcoxylx1MDAwNXhOI+k2XHUwMDFjIeBigujFzVx1MDAwM1JR5ONWSMVkbP5TXHUwMDA30dFrOfKVr4IosK+BqJGJXHUwMDEwXHUwMDA1po1hsFx1MDAwMlx1MDAxN+08XHUwMDFm1DfMzlx1MDAxMfC9Pelntp+zJ/XLtDt6nFv09MjhjFx1MDAwMPxcdTAwMTfspKhH5HrCopbXWignRJrJKLPcXHUwMDFhxtZcdTAwMGakOlx1MDAxMaROc1x1MDAwNcwsb0Y3ZNlFjf1n26rvne9cdTAwMTSueOmymXZJn3FcdTAwMWVjXHUwMDEylyMuRvT4ztgpiIInXHUwMDA1zoZRgOIxvVx1MDAwMOVGo6bH91kzgGrDklx1MDAwMCpcdTAwMTjn3FomYGmEbjrVclH0sFnuPoiGrvLuTbOdcoRyXHUwMDA3XHUwMDFlV4YzriRcdTAwMDXZxKSnV1YjNlx1MDAxOP7DtEB/z1V6QSpcdTAwMTE/1uCr11xmpKhcdTAwMTSSQGq0tMahpl1cdTAwMWGjuXBY367LYj5ifrijXHUwMDA3T2F7eJFyjEopPWSaXGbQkTOrxXTwXHUwMDFlIYomXHUwMDE2QWGYxGWb3rgorjFkI1xm5JdcdTAwMDVGv8rRi0RH76RDcsPY8lx1MDAxMIWHnbBcXK31s+d9YNXidXjb3Fx0U1x1MDAwZVGhlYdLXHUwMDExXHUwMDE3I1d431x1MDAxOUxAXHUwMDE0NPcsQldcdFxuS4lcdTAwMTTbUNCGXHUwMDBinK/1XHUwMDAzaGLkXmryb2pcdTAwMDVcdTAwMWK69aBuXCJ+X70rXHUwMDFjVTqXg44s1EultKslMqJKOe0kXCKAczbJRCnoxIW2hvZGudUphqiRktNfa1x1MDAwN9HY9ua0WFx1MDAxMjhcdTAwMTSJXHUwMDBic1x1MDAwNSN6f7N3cGhcbsdcdTAwMWLKb4mD48vLdtGkXHUwMDFkoyhcbj1cdTAwMDSHldohIZVyXHUwMDA2pMJDrYRcdTAwMDRAoHKKsaLUYVx1MDAxNFx1MDAxN5lFovJ1m/RfRUV5sqBcdTAwMDenXHUwMDE0V9YsXHUwMDFmdVx1MDAxMntcdTAwMWLmLDxcYor9rfb93bFo1Nh9MeWOXjHpaYG60DnlOOr2KUePNEAgNJi0TqCtTS9ENVijXGaYdTOjVibugKJcdTAwMGI0xki9vFx1MDAxNT2KbvuBelx1MDAxOF62N1x1MDAxZotcdTAwMWLRU/78rL7zXHUwMDFiIFRcdTAwMDGnfCZAPynkpKJHfuchbmn6LaN8k9RClFJhjOFq7Vx1MDAxMMpcdTAwMTK5KJdcbmjThS2/R39zONgt14e1sGev9pvuaqvVPL5Ju6NHpulpY1x1MDAwNSolri1cYmWmMGo8JizOXGaCh1x1MDAxMolSi1HBnEQzis5vzUBcdTAwMWGH4HRklMLBpGSXt6PhzXPxWFfgtKFcdTAwMDfN6DjXuO9cXJ+m3I5mLEpcIs0kt1JSzGZcbqKOeYZrNLS4XFyFVelcdTAwMTVMXG6kcFoquWZcYrWgklx1MDAxMMqtMEY5sULC6N2gkr1cZrrc3lx1MDAxNp5cdTAwMGWCutLnxZ1fXHUwMDFjXHUwMDE3XVwi1cmgXHUwMDFlkkjKXHUwMDExg1bZOD5eMMqRXHRoh1PDUfHHbFTaMIpcdTAwMTetlcqu3Vx1MDAwNmgyRonbaK2X313yuzVVubjIQa691dbFfK++U95Lu1xy5cxDpums5lx1MDAwMijhLuYz6FxyLIJcdTAwMDdlXHUwMDE0in2UzIid9JJRq61hbt2oqLHJWSRcdTAwMDKnXHUwMDA0OZhcXF7PO1x1MDAxNtajXnnX+IfVgOWvK73MYSvlXGJFXHUwMDEzioJQOEAuXG4uvttLL3eKeU5cdFxcq0owmeJcdTAwMTRcdTAwMTLU8UhF5PrF7WPhpFx1MDAxOVx1MDAxYSosY1qsYELLrTq/61x1MDAxY+RcdTAwMWHPmyeNu3774Ph6J59ygGaM9Vxmulxu9N9cdTAwMTaZJlx1MDAxYdIphEqPNL41XHUwMDAynLZcIr3ZooIhmWaCs7UjojJxf14xYFayXHUwMDE1bOh5/iZz8XB9tN+v3PT2j/1wMDjup1x1MDAxY6JcXKBcdTAwMTFF52iYQHzqqc1P9JyolFDpo1x1MDAxMFHO8PRm3Fx1MDAxYmHAraFcdTAwMTGNf9Xp3Xmh0LdJvTxAd/bt4O4y18z3/dPCxuZF9bbXTHtIlFx1MDAwMEqln1x1MDAwZVxykKSakFx0gFwi+1x1MDAwNI9JQCNcdTAwMGKgdDxJL21cYuWa/oBbPzGvXHUwMDEziSg4UFx1MDAxY9n30lxi7e9cdTAwMGWi67P9u/x20d86uL85qNRL1ylHaMZ4XG59o0J3XHUwMDAxWlx1MDAxYjeVy4xcdTAwMTA1nlX4w5DpoZZKr1x1MDAxMUX7L1xmY2uXhWdE4r6SZVKD4CtU0LvDXHUwMDA33ogye4ftx5NwN5dpXHUwMDBl4fYk7dEm7lxmldAz9PJcYtU463lcdDZcdE9xXHUwMDE41dyNNjHSi1COa4ip9YuIuuRcZlx1MDAxMqesXHUwMDAy59zyfr5cdTAwMTjUud/KZINmvlx1MDAxMEB+b7flXHUwMDA2v1jML5PkpD1cdTAwMDQgUVH09256W1x0ISpcdTAwMWS3XHUwMDA0XHUwMDFlQbs2qYWo1lx1MDAwMlDR2bUzoi7RiDpcdTAwMTRKkmoklkbo9eVppeVOq3dXwVnhXHUwMDAyoPPc2bxKuZ9cdTAwMTeadjZRryuFpFx1MDAxNMnmZMTe4mWFQlx1MDAxZeW+XHUwMDA0kWZcdTAwMWIqqMKMXGLzmiE03lx1MDAxMmCmgFx1MDAxZTUuuj29vJ8vXFxcdTAwMWa0d4P93bN8Z/9cdTAwMTAuovOry27aO+VIXHSe5VxmaSgyUmXdpFhyinuWMkuEJFef4pAol5Kj29Nf14Xk1yfhXHUwMDE5So7UdoVke7d53bzs12+jsti0e7Xn8rY/eEw5QrmzXHUwMDFlSnnDnFx1MDAwM1xynEs9XHUwMDA1UeWh+1x1MDAxNHgrXHUwMDE4l0KmV847XFxnhpm1I6JxozFTVudw3lx1MDAxONjljejJUc70MvKKXzzdN1x1MDAwNyYrovKen3KIKobwQCGM8yuYxEmejok6T1x1MDAwM4p6pKia9kbTXHUwMDBiUdBKOFxy65bJ7JLTRFFcdTAwMWRKSuJd3ojulMNm5/G4nLX8YPPsYfdhsFx1MDAxZJ6lXHUwMDFloeTmkYkyg2aIs0lcdTAwMWI6ioniSkXzqpyWMr2pI1xc4PJB7bB2RtQlt2xE5uVor2V5XHUwMDFium1y/WKws/1cdTAwMWPehe2gye3u3W4j5VxiRevpMdTrXHUwMDAyZ1x1MDAxZt04s2ZcdTAwMWGitDPqJFx1MDAwN8DZifO81GFUUXWqiG+zrFx1MDAwN0alTNxZQu5FU6NWXHUwMDEw9PV+aT97aMPhTXk7a1x1MDAwZUr2LOcnZThNgW1cdTAwMTKi08mYb6+qlrp1f1x1MDAwNYyad+P2VlDASYAyXHUwMDE0d5qO21NtsuZUUmlcdTAwMDDExC7GXHUwMDBmYDTqlJrddqmDXHUwMDE4mMUpgPK0Nlx1MDAxNlxmt05CrI7/XHKniFBv1FFcblx1MDAwMMkzn9dyjOrMjFCfh9PXXHUwMDBiY2DF5rtlbvaOb8RG+/K8e1lcdTAwMGI6rqnz4+ZdXHUwMDEzKCx1Oq3B97crf/+56H3z14f3rHSlj+rnXHUwMDE1dtIuR2FcdTAwMGX2l3vf1/8lry7K+bWgv2R1cctcdTAwMTLTXHUwMDA3jbC0YblCM7+N21opujs5Pm9l6rJcXMmen++qpFjEj62un9HKT2lcdTAwMGKgrUKVXHUwMDA3bqpcdTAwMWNQWk8qLt2ojilcdTAwMWWK+lx1MDAxMftfq9VmXHUwMDE3lVTWXHUwMDAzyv1cdTAwMTIkMy23s4tcblx1MDAxZJPnqJBcdTAwMDG1psZJmVx1MDAxM4dwgkpuYd2Mv1vU3cdYUt92+dwsU9nLNSvlKHrK9a3eeK5cdTAwMWWq3jDt+1xyXHUwMDAwnpTURVx1MDAxMuGnQGkzyaK14p401F9cdTAwMWJcdOpEzXXaXHUwMDE4iuFSMbV2m7bOJkJU472a7F34XHUwMDFlQvc3XG79QfhQUMWbw+3sZkFtmrNK2lx1MDAxMcqd8lx1MDAwNMp3SX0nXHUwMDE4XHUwMDE3alx1MDAwNqCMa8GQqVHBdZopNCVHOFg7XG7tYjd9JvNFkFx1MDAwNOfLQ9Re7uYvh49mb3PrPNdX16Hc2vvFPdKW2bRcdTAwMTWeQdvjkFx1MDAxZjvjzHTHXiU8SdWA1qF3TXPzXHUwMDE0R1vs6Fx1MDAwNNbNivJ4i9rpbVvNrY1ff1x1MDAwZqLDp42HsFx1MDAxNO5cdTAwMWNcdTAwMTTPhsXipSuHh7bzIzR0Olx1MDAwNeqn0lDG8J4jXHUwMDAzdEDJ1lNeXHUwMDFleSjScYNcdTAwMDDVlF3wM2io8UDhXHUwMDE4JHVoXHUwMDAzNUfbceLCklx1MDAwYomyXHUwMDBlZSaPl9K85lx1MDAxNFxiy6RUn7hcdTAwMWT2emGuXHUwMDA2XHUwMDEzYviU63b5+UA1+cHTyYE7XHUwMDBmup+g7VrX1cst8Vh7PKpcdTAwMWY7rVub+iR/8WnaTlxuiDVcbvrgmvLDMGh3568oyZM7XGYr5yxzq2RCbGX2nvfPj1xucnhcIrv3xXZ2cFx1MDAxYyZF91x1MDAxNq6przP6XHUwMDFhiVx1MDAwN51xXHUwMDAx1Pg03lbwxeRcdTAwMDPSXHUwMDEyVE2oqsxcdTAwMDerv2slwEmdXVMk7HDJ4utcdTAwMTUyo/hpXHUwMDFhb2vKok9cdTAwMTBmVDw56icvp5dcdTAwMTR3Qo1cdTAwMWF7ft7uyE9aU0tgXzmIybWfif3kM1xuwFx1MDAwMUi1SmD7iVx1MDAwNTf1/oHhfJBcdTAwMWLWXFwh2+tdi3RD36CppqpxjdCmcpjp9u9I2KnXoXVcdTAwMTLR9cFk9FqpjG7j56Cfmk9q+Mxg4ZfRnUXwVFwiMVx1MDAxNVx1MDAxZIU8MvJ4R5J3W8JcdTAwMGVPLs+u77rNfKd5oSHT2Fx1MDAwN5tNNzyd9tBgSsGMY8qJKXBKz+JcdTAwMDLmhrLUwHysv1x1MDAxMVx1MDAxN2Vr55Dxz1x1MDAwMKdQynKxhkG35NMzuFx1MDAwNGXUSlx1MDAxNbuP+9lgj4iDuXlu9jbEw0Bmf3FcdTAwMDeuJei4RlxmOjlSjFx1MDAwMPFzw0ZbNnRMXHUwMDE2JZOiiEO3luL+RpxcdTAwMGJNmzFr1+Ao3rBnZldcdTAwMTBHwrhZoVZcImtcdTAwMWVcdTAwMDPjXHUwMDBlaq2to63raCNo+LntX9xVe7mYhpNcdTAwMDBcdTAwMTZ1I0V+J308NY+hXCJcdTAwMWXUaZRcdTAwMGKV5iRKXHUwMDFjnTSW/Y5OfnHnd5VYK2GFcJrqXHUwMDA3loZo4TTfk4em8LjNdjZzuX5hUyRWnKXEy4PxkFuilURHayF++ucoP1xyXHUwMDA0XHUwMDFhUW2onSOdifkhfFb8qqqWZvEpkelKw6TRQlx1MDAxYlxcLLG43zisQf1cdTAwMTStQ7pB4Vx1MDAxNdBaTyN0XHUwMDE09TCSr1vQzSVHXGJIJFlcdKvQUD9T4mf3NmpfPcnNnWHtML+dej+PPt4zQlx1MDAwM50wRKdoiNnuRoxpOslNKzRT6W1cdTAwMTJnR1lcdTAwMTl67WwofudELsrwnjnrlofoTdjdkNnh9uVcciuUjrWJStlccpluXHUwMDFiyqX1kMFcdTAwMTmJ5lx1MDAxMlx1MDAxOY2FqSRfQLFkUU4yZpHruY95eVXRfm2OkueS46eAXHUwMDExTvBRavWcXHUwMDFjXHUwMDA1OrVTMFxyXHUwMDE2mLPU3l1NQ1TTQYlcdTAwMDBfd9jgV1lRkSiXlFx1MDAxMeji2FxuPbVvXHUwMDFlh0enN1x1MDAxYqWDKLeZk1d3bGuHp39cdTAwMDNYSI+yeYGSza1cdTAwMDA23WpcdTAwMTM8bZWUXHUwMDEyvT3EXHUwMDBmvkubXHUwMDE15Vx1MDAxNI8wZv16XHUwMDFmOJG8XHUwMDE3XHUwMDAwVlx1MDAxMEWC5UFcdTAwMWFtXHUwMDA1R/WgWzdcdTAwMWI1lW1l7lx1MDAwM3Ppl9NcdTAwMGVS9PJUsoN+XFxcdTAwMWGlXHUwMDEwitMgdVx1MDAxZdovxCqFdOJN5NJcdTAwMDZSnCluKVx1MDAxZnTNMFx1MDAxYT9cdTAwMTFipnCXXHUwMDBiZsRcbudk1bcv3P5Teeu2pE6e29cl/7Rym3TydUpcXL202lx1MDAxM1x1MDAwMtmopSTe+Fx0taOwKGhUMlx1MDAwZUWIZFoy9zF8Jukl9OKKsk81UJqhdrGaorGnd4rO6zLcKK6EZmZ2z1xuIU69XHUwMDBm1q9jMSzYVjKGsVWOeLnh2UplWDhcdTAwMGVOs+y+556zXHUwMDE3YifpQMzUQFx1MDAxNI0kbeRcdTAwMDFyUif1JFx1MDAxOdWkllx1MDAxMKD2RTzqj0Xuk8go8mHlLNVIcyRXLFZcdTAwMWQwRihjnjI4PIaTgsYyXHUwMDE2vv2nlyGaeerR8Fx1MDAxYpacRf4wmodNnlxcVa6RkFx1MDAxYlx1MDAwMSvkIZ5W84/Hd2ew275cdTAwMWJcZlx1MDAxYvno3u+ap3SXSSiH2ETLiV7CUidcdTAwMTI9mUFjXHUwMDE0XHUwMDE19DKgvIdcdTAwMGZcdTAwMWYnPDeDRsBcdTAwMWNvLmeCSWhTXHUwMDExXHUwMDBln1x1MDAxOUx6vfCl9Vx1MDAwZpfZU13JVEphsSOHw61iodhcdTAwMTPnn5QnoLWlLmorLJgxMFvN6Dx4XHUwMDFlbVx1MDAwM9iJR7OlRlx1MDAxMD5NgGu0knCA/aBcdTAwMTP1SuFtXHUwMDE3X+i9zlxcbPq7Plx1MDAwZWD0jjDx0o0wuKOF9z30a5MrMlxu8Ma8XY5asa2EXG5cdTAwMGWlhG/X2a9Of6VWJ7hcdTAwMGKapfBiwbBcdTAwMTZahpdcdTAwMWI9xzTIZIVcbijKhNErdDFfjKhfkFxc965lQD3qIaqUQJ+BtMRNXHUwMDE1+Vx1MDAxOVx0XHUwMDFlnYdhkfcjq5FcdTAwMWZzW/OT69AvXG5UlYBcIphROs+c7Wb0q55DgkFcdHbKonebsVx1MDAxY+SxjLGWfYXXWr1cYil220qdaDNoVoPmXHUwMDFkXlx1MDAxY1uP7/7LR+8v4WPoy5XaL5SUXCLRXHUwMDFj74xDT6rHZ3KNVnalR18kwzzkxXRwl5OG+KtcdTAwMTWvT3ozY9/9ZvX9QS2uXHUwMDBmfFx1MDAxYpTknlPc4mxcdTAwMDCXbsxB4kPiXHUwMDFl51Kh1lx1MDAwNIlUmrJtZoZcdTAwMTSWutFWq9FcYlwivPUnraBcdTAwMTlN3+LRvdyghV33SzMmXHUwMDAzv1L82rRcdTAwMDVo0ztOuojx/76NV8nol7f//+fPuc/OJIN4dHlcdTAwMTa/43f8I/7vyuaLJ1x1MDAxZk5Le/Ocs1x1MDAxNWLAi1x1MDAxZFc6zZfzUPMpyv3WUnOY2qOw1mPMKGGRknNmf0KJXHUwMDFh81x1MDAwMFmToNguoLDjZk5ysGTCXHUwMDEzmo7w1pyaUdhZ88XpQHYrxdeR7vH9/lx1MDAwNebrXHUwMDFmQ8A8a1x1MDAxZLlZh8JeUZnujDlBTcM0amqUKiSqtVtsvCa/xW9kQ5KBRD+zXHUwMDEwWtGCJEmjeC/TmYQmXHUwMDAzgKZ8XHUwMDA1bdQqPF01TMtvV/LX3WM1dJC5/KGd+K/TRijbjUPNjrLIWlTHkzvxVDXoKapcdTAwMWWUqKul+mDsc744UkuJI0pVNWA/8Vx1MDAxMIw3XGJ9blx1MDAxMfdCcXT7iFx1MDAxNL+xVb3bvkV/uXdxcrxfqX+aOHLUzunXiKOX2UybNnpcdTAwMTnVj3FcdTAwMGJcdTAwMDHJXHUwMDA1xki0KTNpeW6xXHUwMDE4T2nkXHUwMDE2UiGro+OaKVdcdTAwMWOMXHUwMDEw05ZBXGLPoltS1OROfvTwnLmWgTPt0d4yMmlEuFJsjqHgnkZSqdExUltL62LBk3+iKsZcdTAwMWEmv2Lb7udcdTAwMGKjxVx1MDAwZeaNNDhPOGOENnzUko/NXHUwMDE1IcxjXHUwMDE0XHKj7Fx1MDAxNkZpoOh0XHUwMDE3c4tcdTAwMGZcbiNhPY5uxpGfMZKOvJkvjZhjtNGArFxih6V/c2mUXGZh+snMondFXpOsjExihqFcdTAwMDGUwtbEsi3es16LvVZcdTAwMWGtl1FcdTAwMWX1t2NcdTAwMDJcdTAwMTSDiVx1MDAwNnKvxst4xjJcdFrhXHUwMDE34vDBXCKvucZcdTAwMGJQ5VtNMLa4xOLNKOLGyzBljdOSmjHiQGaMXHUwMDE3woK7rzon+Yf5w2eZr5hxoiOEXHUwMDA1MlPpmKXOXHUwMDAx8djOa/hcdTAwMDfoaFwiOnmIW8Glcu/Yr99WXHUwMDFiZZKx9HJ5XHUwMDA2RitakWR1lGhEqK1cYt56I5a3XCIn9d3BMCzmn56et0Bu5Fx1MDAxNTRr27/Sirh3xZFHLcQsXHUwMDFk0IbSR05ZXHUwMDExoM5cdTAwMDHGOIfYQ1x1MDAxMfNTjMg8qzFHXHUwMDFijVx1MDAwZWVxnymOXHUwMDE27sHgLVx1MDAxOfOpny0zupVOK1xmb1u1WtdPxe7L5IB+zD1Llth3QzCEnEKvtHyRyuLS3DT654xcdTAwMDGPS+CS0qe51GoyX0A5Ntqx1VYoZDL8J0QuueCe0tRXXHUwMDAx7eaoXHUwMDFi7lx1MDAxY/+spWfokGVj0Fx0aTnbXkvQSVx1MDAxZUZ9zYEzeFx1MDAxZoT+sd3PKVx1MDAwNz3tyt533YuL/OOuW1ErXGIrkF+imcDbXHUwMDEzXHUwMDE3XHUwMDFmb45bOmqpwoxcdTAwMDQpUVxm/N5cdTAwMWM/XHUwMDExSaOrM1x1MDAxOPok56yTc4uVo0TCXHUwMDE1OvecXT1cdTAwMWM37/qXwX3vJl9cdTAwMDJ5UeVcdTAwMDdhql1zhjvnOS6cpoP90PPZ6Vph56HWZNbRXHSq8Yq/T299OVx1MDAxZf9cIlx1MDAxN40q0Fx1MDAxOfeZOe5vOEp9o5KPXHUwMDExgKd/szS4fVx1MDAxYcaPOfvkNnCoPo0wYoVcdTAwMWSGhfOazoUqXHUwMDE1ddDiXHUwMDFjkKFS+e1kIFx1MDAxMVWnZ9FowqhcdTAwMTbQsVx1MDAwZlX7LVxcqNyiqmR07rmQjDNcdTAwMDVz8lx1MDAwM9Uoo4DqpTRSMJjTT9MwwVx1MDAxY1UnfonD/+GVtqRcIl9s+L/Ftyq5odwrTcE7bpRSc/y68jRnXHUwMDE2hTgqVVx0+m2XbsWA4uImXd8mczuorVx1MDAxNEVcdTAwMTU5XHUwMDE31up5XHUwMDFiqFx1MDAxMlx1MDAxN1x1MDAxOaP+fdRa38rfm2skYph+MjPwTeJcdTAwMWF/vH7C91K7fVx1MDAxZSHo3qZcdTAwMDPhXHUwMDFjVF9t9Phrfu9cdTAwMDf+YHM+tyZ6/cfrXHIlXHUwMDBi5NOX/e/ff/z9/96SXHUwMDBlNiJ9 + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1da1NcIsuy/b5/xcScj3dTp96VdVwibtzw/cQ3Prj3hMGjXHUwMDExtFx1MDAwNYRG0Vx1MDAxM/u/30x0S/NoXHUwMDA0RaZcdTAwMDdnwlx1MDAxOWnAomtV5lpZmVn/+ePHj5/RczP4+a9cdTAwMWY/g26pXHUwMDEw1sqtwtPPP+nxx6DVrjXqeEn2fm43Oq1S75nVKGq2//XPf95cdTAwMTdad0HUXGZcdTAwMGKlgD3W2p1C2I465VqDlVx1MDAxYff/rEXBfft/6PtB4T7472bjvlx1MDAxY7VY/5dkgnItarRef1dcdTAwMTBcdTAwMDb3QT1q47v/L/7848d/et9jo2tcdTAwMDWlqFC/XHSD3lx1MDAwYnqX+lx1MDAwM7RCXGY/etCo91x1MDAwNmtcdTAwMDRcYrDGqPcn1Nrr+OuioIxXKzjkoH+FXHUwMDFl+nm6X1x1MDAxMfXgtntcXFrNtM787bZpXm/1f2ulXHUwMDE2hqfRc/h6J1xupWqnXHUwMDE1XHUwMDFiUztqNe6Ci1o5quJ1MfT4++vaXHK8XHT9V7VcdTAwMWGdm2o9aLdcdTAwMDde02hcdTAwMTZKtehcdTAwMTlcdTAwMWZT/P3B13vwr1x1MDAxZv1HuvhTXHUwMDA2JDNSXGIvXHUwMDE0SK3k+0V6tVx1MDAwNmDaOKe4kpJ7I9TQsNZcdTAwMWEhTlx1MDAwNFx1MDAwZetcdTAwMWa899VcdTAwMWZYsVC6u8HR1cv950hcdMXA9p/z9PZhrWVcXDtntNRcdTAwMDYs9/r9XHUwMDE51aB2U43wKU4yUMZya7nkXHUwMDAw0Fx1MDAxZmc76M2HXHUwMDE0Smu87t37XHUwMDE1+vXNnXJcdTAwMGZcdTAwMWH/jt+xevntjtU7YdhcdTAwMWYxXdiIwan/mk6zXFx4nXZhcZjaWqO89u/Xw1r9bvjtwkbpro+U3qN//flcdIQ6gCSEXG6rXHUwMDA1N8JyNzVE13OVu9vuysFDVl1cdTAwMWOfVs5cdTAwMWXzufWVlEPUWCaMdlZp6Vx1MDAxNN5cdTAwMGVvR0GqlDdKIE6VXHUwMDEzNrUgxeFr77k1S4ZR62RcdTAwMTJGvZPOOyfF1Fx1MDAxMFxyzrZ38e5cdTAwMTWLZX1ylN2Bvb2jO51yiGaEYlxceDBeOHRcdTAwMWFcdTAwMDBiXHUwMDEwo8Yqpi1wvCQk6OFxpVx1MDAwN6HCKFx1MDAwNfhHLFx1MDAxYkKtT0Ko4Fx1MDAxYX1cdTAwMDc6uOlcdTAwMWT95WHxYO/KQLFxdHxfuHzcRn7T/rVcdTAwMTBcdTAwMTX8QzOqmTPIZyRY4dFzuFx1MDAwMYRa5Vx1MDAxOZJcdTAwMDDv0UJxo41MLUSlcE7jUJdcdTAwMGWikExFJTo9iHObj1x1MDAxMHqfyVx1MDAxN9RmaSV3mq/eXHUwMDA169Wjja1QpN2IgmDIQjUoK1x1MDAwMVdlzEz2XHUwMDEwqjlDXHUwMDBiqo1cdTAwMTZcdTAwMWGXqzOpRahwOF1cdTAwMDZ9wNJxUZPMRVx1MDAwMY1cdTAwMDbeL+Gnxqh3XHUwMDA1a8LG7W2nlFx1MDAxM9dhp9w4dFx1MDAwNynHqJCGeYuk21x0hICPXHSiVzdvkIpcIlx1MDAxZlx1MDAwN6k0V7H5T1x1MDAxZER7r1x1MDAxNchXXHUwMDE2XHUwMDA1UcNcdTAwMTdcdTAwMDNRp1x1MDAxMiFquHWOm1x1MDAxObho62W3uuI2XHUwMDBljNjeVkFm/WXzqHqedkePc4ueXHUwMDFlOZyTXHUwMDA2/zUwKOpcdTAwMTG5TFx1MDAwMmp5a6X2UqaZjHJcdTAwMTDgOF8+kNpEkHortOFuejO6ooo+ut95gUZ1+3QjdyFcbuf1tEv6jGecK1xcjrhcdTAwMTjR43tcdTAwMDdDXHUwMDEwNUxJnFxyp1xyisf0XHUwMDAyVDiLmlx1MDAxZd9nyVx1MDAwMGpcdTAwMWRPXHUwMDAyqORCXGJcdTAwMDAuzdRcYl31uuGj6G612L6T97Ys2lf1ZspcdTAwMTEqvGFCO8GFVlx1MDAxNGSTg55eg0VscPyHW4n+Xuj0glQhfsDhq5dcZqSoXHUwMDE0kkDqrFx1MDAwMudR006N0b2wW12vqnw24kG4YZ+ew2b3LOVcdTAwMThVSjFkmtygI+dg5XDwXHUwMDFlIYomXHUwMDE2QeG4wmWb3rgorjFkI9yohVx1MDAwNUZcdTAwMTfl6GWio/fKI7nhfHqImruNsFiuPG6ePlx1MDAxYV7OX4bX9Y0w5Vx1MDAxMJVWM1xcirhcdTAwMTiFxvvOzVx1MDAwMESNXHUwMDE1XGZcdTAwMTC6WlJYSqbYhlx1MDAxYeuExPlaPoAmRu6VJf+mZ7Cha3f6Klx1MDAxMrflm9xBqXX+1FK5aqGQdrVERlRrb71CXHUwMDA0XGLBXHUwMDA3mShcdTAwMDWdhLTgaG9UgE0xRJ1Sgr4tXHUwMDFkRGPbm8NiSeJQXHUwMDE0LsxcdTAwMTmM6O3V9u6+y1x1MDAxZK7ooCF3XHUwMDBmz8+beZd2jKIoZFxiXHUwMDBlUNYjIVVqXHUwMDA0pJKhVkJcdTAwMDIgUTnFWFHqMIqLXGaQqCxuk35RVFQkXHUwMDBieuO1XHUwMDE2XHUwMDFh3PRRJ7m94k7C3Vr+ca15e3Mo7yv8Np9yR6+5YlaiLvRee4G6fcjRI1xykFxiXHKuwEu0temFqDXgtDNu2cwoqMRcdTAwMWRQdIHOOWWnt6JcdTAwMDfR9WNN33XPm6tcdTAwMGb5leg5e3pS3fhcclx1MDAxMKqNoHwmg35SqkFFj/yOIW5p+oFTvklqIUqpMM5cdL10XGLliVxcVChtaNOFT79Hf7X/tFWsdithXHUwMDA3Lnbq/mKtUT+8SrujR6bJrFx1MDAwM4lKSVgwUrshjDrGJeDMIHgokSi1XHUwMDE4ldwrNKPo/JZcZqRxXGJcdTAwMGVHRilcdTAwMWNMSnZ6O1x1MDAxYV695Fx1MDAwZm3JXHUwMDFj39unenS4d3/bujxOuVx1MDAxZM1cdTAwMDBKXCLLlVx1MDAwMKUoZjNcdTAwMDRRz5lcdTAwMTNcdTAwMTZcci0uV1x0Or2CSVx1MDAxYiW9VVotXHUwMDE5QsHoJIRcbpDOaS9nSFx1MDAxOL15Km2e19pcdTAwMDKuc8+7taq2p/mNX1x1MDAxY1x1MDAxN51cItXJoVx1MDAxZVJIylx1MDAxMYOgIY6PV4xcbmRcdTAwMDLW49RcYlT8MVx1MDAxYpU2jOJFXHUwMDAwpWHpNkCTMUrcxlo7/e5S0K7o0tnZntlrrjVtPtupblx1MDAxNLfTbkNcdTAwMDVnyDQ9WCFcciXcxXxcdTAwMDa9XHUwMDAxIHhQRqHYR8mM2EkvXHUwMDE5XHUwMDA1XHUwMDBijvtlo6JcdTAwMGWSs0gkTlx0cjA1vZ73PKxGneKWXHUwMDBi9ss1nr0sdTL7jZQjXHUwMDE0TShcbkLpXHJyUePju730cq8581riWtWSq1x1MDAxNKeQoI5HKqKWL25cdTAwMWZcdTAwMGInjdBQXHScWzmDXHQtNqriprW7d/+yenR/89jcPbzcyKZcdTAwMWOgXHUwMDE5XHUwMDA3zKGrQP9ccsg00ZBcdTAwMGUhVDHS+OCk8Vx1MDAxNmR6s0UlRzLNpeBLR0RV4v685oaD4jPY0NPsVebs7vJg57F01dk5XGbCp6fDx5RDVEg0ouhcdTAwMWNcdTAwMWSXiE87tPmJnlx1MDAxM5VcdTAwMTIqfVx1MDAxNFwi2juR3ox7J53xS2hE41x1MDAxZnV4d15q9G3KTlx1MDAwZtCNXHUwMDFkeLo536tnXHUwMDFmg+PcyupZ+bpTT3tIlFx1MDAwMEqln1x1MDAxZVxykKKakFx1MDAwMYBcIvs0jCuDRtZcdTAwMTht40l6aUOosPTH+OVcdTAwMTPzNpGIXHUwMDFhb7RA9j01Qlx1MDAxZreeosuTnZvsej5Y27292i1VXHUwMDBilylHaMYxjb5Ro7sw1jo/lMuMXHUwMDEwdVxmNH5xZHqopdJrRNH+S8f50mXhOZm4r1x1MDAwNFxcWSPFXGZcdTAwMTX0fv9O3EeZ7f3mw1G4tZepd831UdqjTcI7KqHn6OVcdTAwMTGqcdbzXHUwMDFhbJJMXHUwMDBi06u5621ipFx1MDAxN6FcdTAwMDLXXHUwMDEw18tcdTAwMTdcdTAwMTH1yVx1MDAxOSRegzbe++n9fL5WXHUwMDE1QSOzWatnczWT3d5q+KdfLOanSXKyXGZcdTAwMDFIVFx1MDAxNP29XHUwMDFm3lZCiCovgMAjadcmtVx1MDAxMLVWXHUwMDFhVHSwdEbUJ1x1MDAxYVGPQklRjcTUXGK9PD8uNfxx+eaidpI7M6b10lq9SLmfl5Z2NlGva42kXHUwMDE0yeZgxFx1MDAxZfCyRiGPcl9cdTAwMTmZZlx1MDAxYiqpwoxcYvOSITTeXHUwMDEyYKSAXHUwMDFlNS66PTu9n89d7ja3ajtbJ9nWzr45i04vzttp75SjlGEgONJQZKRcdTAwMWH8oFjyWjCgzFx1MDAxMqnI1ac4JCqUXHUwMDEy6Pbs4rqQ/PokPEfJkVx1MDAxNmZItverl/Xzx+p1VJSrsF15Ka5cdTAwMDdPXHUwMDBmKUeo8MBQyjvuvbFGXGJlhyCqXHUwMDE5uk+Jt4JcdTAwMGIlVXrlvMd15rhbOlwiXHUwMDFhN1x1MDAxYSNldVx1MDAxZeeNXHUwMDFimN6IXHUwMDFlXHUwMDFk7LlORl2Is+fb+pPblFFxO0g5RDVHeKBcdTAwMTDG+ZVcXOEkXHUwMDBmx0Q9s1x1MDAwNkU9UlRLe6PphaixWnprli2T2SeniaI6VJTEO71cdTAwMTHdKIb11sNhcVx1MDAxM8Tu6snd1t3TeniSeoSSm0cmylx1MDAxZJohwVx1MDAwN21oLyaKK1x1MDAxNc2r9lap9KaOXGKJy1x1MDAwN7XD0lx1MDAxOVGf3LJcdTAwMTGZl6e9lult6Lrbe8zXNtZfwpuwWatcdTAwMGLYutm6TzlC0Xoyjnpd4uyjXHUwMDFi5+CGIUo7o15cdGNwduI8L3VcdTAwMTjVVJ0q49ssy4FRpVx1MDAxMneWkHvR1OhcdTAwMTlcdTAwMDR99bGws7lcdTAwMGZh96q4vul2XHUwMDBicLJcdTAwMTckZThccoFtXHUwMDEwosPJmO+vKlx1MDAxN9rVYFx1MDAwNoy6XHUwMDBm4/YgKeAkjXZcdTAwMTR3XHUwMDFhjttTbbJcdTAwMTVUUumMkVx1MDAwM7tcdTAwMTifwGjUKtTbzUJcdTAwMGIxMIpTYzSz1oFxXHUwMDAyvDKxOv53nFwiQlmvo5QxSJ7FuJZjVGfmpJ5cdTAwMWZO3y70gVx1MDAxNZvvhrvaPrySK83z0/Z5pdbydZvtN+9cdTAwMWFAYaHVajz9fL/y15+T3jd7uX/LXHUwMDBiXHUwMDE39qB6WuJHzWJcdTAwMTTumZ3p3vftf8mri3J+wdiFrC5cdTAwMDEx2jhcdTAwMWOMXHUwMDEwTkol4onnXHUwMDFmNlC5rlx1MDAxNKKbo8PTRqaqiqXN09MtnVx1MDAxNIz43PL6jl5+2oIxXHUwMDE2NMo844fqXHUwMDAxXHUwMDE1MKWF8r1Cpngs6jNcdTAwMGWgUqmMriqlgVx1MDAxOUr+kqQzQcDoqkLPxDxVMqDYtFx1MDAxMsyYQISXVHNrXHUwMDE2tKouy+dr8qHycFA99NY2Vu1R9mwu6EfB4rXQseLgb+U/k5pcdTAwMDc5IHFcdTAwMGbTp3650vZevVSMoue9R7ArL+V93emmfTvDXHUwMDE4plx1MDAxNDWpRHBro61cdTAwMWIk6VZcdTAwMGKmXHUwMDFjte9G/jtQ0p02XHUwMDAy5ITSXFwv3Z6wh0SIWrxXg61cdTAwMTE/QujOSu7xKbzL6fzV/vrmak6vupNS2lx1MDAxMYpcdTAwMDaBSWucorZcdTAwMTZcXEg9XHUwMDAyUC6s5EhcdTAwMDSpnjvNXGadci+8WTqG7lVcIoUwXpLCXHUwMDE300NcdTAwMTTOt7Ln3Vx1MDAwN7e9una696gvQ7W2/YtbsE2zJyyZQ9vjkX57591wQ2AtmaJiQ/Dou9Pcm8XTXHUwMDBlPjqBZbOiXCLeXHUwMDAxd3hX2FxugPj1jyDafV65XHUwMDBiXHUwMDBi4cZu/qSbz5/7YrhcdTAwMGatz5Dc4Vxmq28luZzjPUd+6Vxy5XJcdTAwMGZ5eWS5XHUwMDBlXHUwMDA1JFx1MDAwMtRS8sJ3kFxcx4zGMShqXHUwMDAwZ/RcdTAwMTjpKIhpK4GKXHUwMDAzqEuMiFfqvKUsSOBK6Tnutk1cIrlSdp/32m1x+qTrYvf5aNef1trTkdyJ0vH7yDNJRyVNrFx1MDAwZtFcdTAwMTfXVFx1MDAxMIa1Znv8ilJcIrmBsfZcdTAwMWW4nyXRYi2z/bJzepBT3SPVvs03N59cdTAwMGXDpODhxDW1OKNvkXjQXHUwMDExXHUwMDFhhvqqxrtcdTAwMTa+mnyDtFx1MDAwNDVcdTAwMTlqNvfF4vJKweCkjq4pko24ZPH1XHUwMDFhmVH8sI73NVx1MDAwNehcdTAwMTOk69Vm9trVq+ElJbzUvb6h89t8+aY1NVx1MDAwNfa1NzG59p3YTz5cdTAwMDLBeGOUniVu/sxrV9XHXSfE01634nObnc6lTDf0XHUwMDFkmmoqSrdcYm2qtlx1MDAxOe4uj4SdWimCV4iuL+a6V1xuRXRcdTAwMWLfg37qbWnNPGORXHUwMDBiozuT4KllYqY7XG55ZOTxhidcdTAwMWZ2nO1cdTAwMWWdn1xc3rTr2Vb9zJrM/Y6BzXTD01uGXHUwMDA2U0nuPNdeXHUwMDBlgVMxwFx1MDAwNSxcdTAwMWMlwVx1MDAxOfe19klCXHUwMDE2XHUwMDAxxpDxeYBTalxyQs4zpJdcdTAwMGUu7pNcdTAwMGbnXHUwMDEwyminZypcYn7Y2axtXHUwMDEzcXBXL/XOirx7Upu/uMHXXHUwMDE0dNxcIlx1MDAwNr3qKUZj4seS9XaE6Fx1MDAxNC7KVUVcdTAwMTGHbi3F7ZOEkJb2epauf1K8XHUwMDFm0MimI46EXHUwMDBiN0MpxqZ7qDm/W2msXHUwMDFkrF1GK7X7YG/9XHUwMDE3N+2eLqbhlTGAupFcIr+DPp5601CNXHUwMDEw6jRKtUpzjiaOTjngv6OTn9xYPrlnN5F6QPc3Q1x1MDAxNvFxtqP2Xe5hnW+s7u095lZlYkFbSry8cVxmuSVaSXS0YOKHi/bS34xEI2pcdTAwMWR1i6QjN7+Ez1JQ1uXCKD5cdTAwMTUyXeW4clZah4slXHUwMDE29+uHNahdI3ikXHUwMDFiXHUwMDE0XjHW2mGE9qJcdTAwMWVOiflcdTAwMDXd3i6MlWB201x1MDAxY5xl1ctcdTAwMGVcbteN64xZ27o+OJ6LXHUwMDA0M1xcgOYm5ry/N3cpXHUwMDE5/lxiXHUwMDA2UGZcdTAwMTaSXHUwMDFiZFxu4uRcdTAwMTai5sWzWt3oVvaz66lnXHUwMDExyCCYk9bQ8Uh0XHUwMDA0iFx1MDAxY23NxLmlY+gsTotMb4c76KWU2KWz0GBcdTAwMTNTl0BcIren7phTQ/QqbK+oze76+Vx1MDAxNc9cdTAwMTVcdTAwMGWti1xumysq3Vx1MDAxNlx1MDAxYWUms1x1MDAxMkWWXHUwMDE2XHUwMDBlXHUwMDE1l4WhXGZlo5mnXHUwMDFlxpwyXHUwMDE34GtKTJdsUFx1MDAxOVx1MDAxMyZcdTAwMTDeM4WKV3hJPaL4mOxcbiFcdTAwMTlCV1rq5lx1MDAwNtrH2/C918rhJzBmXHUwMDAx5yTSXHUwMDFlJnWlXlx1MDAxMM2ViVJMO4nuk8/QXHUwMDBl/Oqhe3B8tVLYjfZW99TFXHJf21x1MDAxMOnfXFyWilFcIrKhPHmQhlx1MDAwZndcdEVcdTAwMDSDVkohkzDxM/vSZkNcdTAwMDXFOpxbvrZccrhukzCKRkNcdTAwMTL9miFBLVqrXHUwMDFkVGvtqlup6M1G5rbmzoNi2kGKPp6qjdCLK6c1QnFcdTAwMTiknkluXHUwMDExq1x1MDAxNC6K979LXHUwMDFiSHGmXHUwMDA0UCrrkmFcdTAwMTRsYlx1MDAwNoTstb6JXHUwMDE3t3yYorx+5neei2vXXHUwMDA1ffTSvCxcdTAwMDTHpeukQ7tT4uhcdTAwMTVYJiVyUaD84/jhur2Qq7Gokjxcblx1MDAxY8Wt4v5r+EzSYkIxTYmz1lCCpPWxvNa+p/eajlx1MDAxYXNcdTAwMDJcdTAwMTmJljYukPtp9I7aNsyx2fLbhbFibDtfs+FZ/SVqXHUwMDE3+NXa8cHGXfelMTcxZlFzLqqkObnfOKWpcWvMXGZUV2yWSt3cYe14k992/MvmmdxIOio0NStcdTAwMDCYsFJcdTAwMGKiu9xcdTAwMGI7SHUtSjXllPROOan813L0k7kudZMmmlx1MDAwYlxidCpcdTAwMDdcdTAwMTljo0FSM0pOJZK9ipZcdTAwMTGuS92mqX3F91fjXHUwMDEx15U4XHUwMDA2mFx1MDAxN0SjoFx1MDAxYo1Dp0guuLcoxJw0M+RQXHUwMDFll7NcdTAwMGaHNydmq3nz1L3PRrdB2z2nu4JEo1x1MDAwNqLKXHUwMDEwiSSCmrTYwexcdTAwMWanqdaZXHUwMDFiytn48knLY7N/pFx1MDAxOYNENVx1MDAxMlxiQ5uNLntRgbDvKlxyOd88tqVMqVx1MDAxMOZbqttdy+fyXHUwMDFkeTqnXHUwMDFjXHUwMDA3a4FcdTAwMWHMzbBg+sBs1KPT2kuPk8DAo5uF+1r4PFx1MDAwMK7eSsJcdTAwMDE+1lpRp1x1MDAxMF638YXsbeZi099cdTAwMGVwXHUwMDAwvXc0XHUwMDAzL11cdGs3tPB+hkFlcEVGNbwx75ejRmxcdTAwMWKkhEMp4Nu1dsrDXHUwMDFmqdGq3dTqhfBswrAmWobXXHUwMDFiPcY0qGRcdTAwMDVsUPRJZ2do8D5cdTAwMTlRvyAx8EPLgHqXIaq0RFx1MDAwNoe0x1x1MDAwZtU/OmVcdTAwMThcdTAwMWRcdTAwMTVcdTAwMDKoK5A1qa+FaMYnXHUwMDA2Mo5uXHUwMDExXHRcdTAwMTeKbE6pSGO2yqXWzDvjKTlQXHUwMDAzWDFiOchjOVx1MDAwN8BcdTAwMTdRQz57fVbstlx1MDAxNVrRaq1ertVv8GLfevxcZl5/9c5cdTAwMTQ+hj5cXKH5Snkpzi3wznhUO7FccqHeyi516INkOEPeTWeaeeWIXHUwMDFmg3x70rtcdTAwMTn7XHUwMDE51MtcdTAwMWZcdTAwMGZqcunk+6CUYF5cdTAwMGLA2TBC+T4vjFx1MDAwZkkwIZRGLWtcdTAwMTRSdcpcdTAwMTRcdTAwMWFcdTAwMTlSWGhHa437+1qEt/6oUatHw7e4dy9XaGFXg8KIycCPXHUwMDE0vzZsXHUwMDAxmvSOgy6i/79cdTAwMWb9VdL74f3///5z7LMzySDuXVx1MDAxZcVv/1x1MDAxZP+I/zuz+Vx1MDAxMsl7gJRXIES8n/xH5muy40qn+fJcZjWlprx1izLbXGbtgCAp59zRXqjzgsM3XHUwMDE073FmkDUhrzLcoHBcdTAwMTRuTGKz4pJJS6ebW0F9OmDUfFx0OqtcdTAwMWVF1OJaYMyySTd38/W3IeBcZsCTm/WANpUqmEfMXHQwXHUwMDE0j6jZXHUwMDA1XHUwMDAwiXbrJ1x1MDAxYq/BT/FcdTAwMWLZkGQg0dcohGa0IEnSKN7mdSRcdTAwMTlcdTAwMGIlXHUwMDFhmvJcdTAwMTm0USP3fHHvXHUwMDFhQbOUvWxcdTAwMWbqrjeZ809lXHUwMDExLE5cdTAwMWKBZ85cdTAwMWJUzdqgbOZ8MIuAKlx1MDAxZZmmykdcdTAwMTT4SukvxlbHiyM9lTiiNFtnYI7ng7xDaL717Vx1MDAxM8XR9Vx1MDAwM1L8+7Xyzfo1+svts6PDnVJ1buLIU6erXyOOXmczbdrodVSf41x1MDAxNtIkXHUwMDE3RyPRpqyq6bnFZDylkVsojayOTrKmPHdDnVx1MDAxMIYsg5RcZijjhfr/qa+eKzTWMlxiblx1MDAxOeCvRiaNXGLXmo8xXHUwMDE0glkklVx1MDAxNlx1MDAxZCN1/Fx1MDAwNFx1MDAxZlx1MDAwYp78XHUwMDFkVXHguFrEtuD3XHUwMDBio8lcdTAwMGXmnTR4Jr1z0jrR61bIx4pcdTAwMTDOOEXDKHeGU1xuKzrdydzii8JIXHUwMDAyXHUwMDEzkvJcdTAwMGLQzzhFp1x1MDAwMY2XRtxz2shQlFx1MDAxN2Hsby6NkiFMX5lR9M7Ia5KVkUtsVO1cZkphcGL6XHKJyV4rjdbLaUat/7g0mpuB3npvxssxXHUwMDA3XFxcdTAwMTmr8Vx1MDAwM1x088VcdTAwMDK1scbLoMpcdTAwMDdLMFx1MDAwNlxcYvFGXHUwMDFhcePluFx1MDAwNuetoj6VOJBcdTAwMTHjhbBcdTAwMTB+UUdIf5o/zMt8xYxcdTAwMTOdriyRmSrPgbpcdTAwMWXEYztv4Vx1MDAxZkOnNtGhTFx1MDAwMqRQ2n9gv35bbZRJxtLr5Vx1MDAxMVx1MDAxOM1oRVx1MDAxMtVRcvNccrQggJiZ4cyQo+rWUzfMZ5+fX9aMWslqU6+s/0oj4j/sPKaYRNugnNCSmuPBUCtxo/DOa6Q+YD1Y578hvjKtOOpcdTAwMWRY4+epjpJ3LSVcdTAwMTfcwSxF1l/TXHUwMDE5qCQqrF1qNcLwulGptINU7MGMXHUwMDE51ec8tUruQCY5rjCNXHUwMDBlavpChslcdTAwMTXGaXTVXHUwMDE5Z5hQRijK01x1MDAxNsrqwdRcdTAwMDHteW/z1oLUSGrENywyIVx1MDAwNdOW2kOgXHTt9VxmXHUwMDFl46qtYo6OonZcdTAwMGX9kVWjPcgknXfi9GKO5cH7IO3nNkKHfPWwV/vYi0/uVVx1MDAxMPfimjpagESqidxcdTAwMTZvT1xch7z7cOWpM1xmd8oohbrg96b7iUjqXVx1MDAxZMHQnPy0SDYhXHUwMDFhx6GUnKFcdTAwMTTq5OLusH7zeF677VxcZVx1MDAwYkadlcVumG4/LYVjSmo6hJvjXz90bpK1llx0R2RcdTAwMDX1p+P8+zqEitjin+SuUVx1MDAxMnrkXHUwMDBic0yof0fS4ttcdTAwMTXSWVSLI1x1MDAwM69cdTAwMWX3OVx1MDAxNVx1MDAxNODvsXzO8Us9oW+YXHUwMDAzXHUwMDA3Wk5cdTAwMWZgnDzHqVxctVS+JdA90KG64NRQ0ixIRl3ikVx1MDAwMCtJNuz71iyyeFx1MDAwZd6j4DRcdTAwMGVoK2SM93eU32tcdTAwMDVcdTAwMWRhKfGvXHUwMDFiSVx1MDAxYlx1MDAxNFx1MDAwZXmxp4rLXHUwMDA1MPAvLLoplfpkJ/AjvoUpPWhqXHUwMDBlj5ZXcydjXHUwMDFl729cdTAwMWavXHUwMDE53jlAfY5cdTAwMDJWXHUwMDE56z6ZgDG5ue7AmEBcboppXG5cXEdIy5xcdTAwMWRcdTAwMWSTY9yjx0D00amCyrrfm3ckYpi+MqPwnVx1MDAxM/GQMnn7VKBittrPcFxuiTzKtNbD41x1MDAxNd9ccnNbudr9bl5cdTAwMDCk24YpYPhcdLmm1sXUXHJg6JhcdTAwMWPnXHUwMDE4OXque7Hfr5X4TTZiMSoxkXj0XHUwMDBlRpvjbsgk3vG9pdZcdTAwMTStXFw47+imiHd0P8k7dKxX2/CilVx1MDAwNmVcdTAwMTh+n554TJ7kNC5aScFUw42WXHUwMDAyXHUwMDE1XHUwMDE5wNCapbPU0U2hwtbKf/HIi4lrVlx1MDAxOdpg7Vx1MDAxZIntpIiXXCL0eVx1MDAwN/pO6o/EXHTz1nM1skEgnCCnOs+k8Y9cdTAwMTbdd6ZOTXZcdTAwMDE/XHUwMDA2ty+N94pcdTAwMDKf6OC1pLlcdTAwMWFx857R0V9KoS9CTT66lThcdTAwMTXvmNxcdTAwMWIkNibqNyC5kFx1MDAwMII6p+uR4YxuUPxOJCNcdTAwMTGv9JVcdTAwMTmF6pw4hpqUosV7jVx1MDAxZvz0u1x1MDAxMFx1MDAxYnZrrds5z8NcdTAwMTU/vburVc15mEs3x5BcdTAwMWFcdTAwMThcdTAwMDWNXHUwMDE0cmjq56JcdTAwMDZcdTAwMTMxPFx1MDAxNaZSQqOToMxcdTAwMTerXHUwMDBiJ3NcZlx1MDAxOFx1MDAxM9yIn/D7VkAl8Ylaz/Fg3UlcdTAwMWPjOytcYmUvM2LxXHUwMDFj48d//V/9dXthcmaVXHUwMDFkfItF0Y5xw/tcdTAwMWNcdTAwMTORyUlcbsiqXHUwMDA1R188fWn7ZCSkcWXjckX14KhpunA8nqree7mhsjTtKDxuuVx1MDAxNMPFnHMkXCKWec1cdTAwMDWdr+SohL5/XHUwMDAz+kSE07neXG7QyjurjLCjlcOKXHUwMDFhTlx1MDAxMn1cXFx1MDAwMFx1MDAxMfnCypySiEz2XHUwMDEzQ0TEgpV0hCO3qPJcXL92+t3xO0b1XHUwMDFlkvZcdTAwMDU0UIX455jI5NL4wUFR0p7GgTlJJ7v50bCMYeg9fS/tg3tcdTAwMDD9e3OTJFxi01dmXHUwMDE0vUnc5I+33/Cz0GyeRoi599lANNfKbya9/zF/PtaCp9XxXHUwMDFijbTX+MfbXHIl81x1MDAxM9CH/c9ff/z1/1x1MDAxNXZx8iJ9 - virtual_size.heightvirtual_size.widthscroll_offsety=0 + virtual_size.heightvirtual_size.widthself.scroll_offsetscroll_yscroll_xscroll_x +self.size.width From fb7c1642bcd8e54f6d402a13163af7b82a603d76 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 4 Feb 2023 15:40:36 +0100 Subject: [PATCH 07/17] Checker example 4, docs and diagram --- docs/examples/guide/widgets/checker04.py | 12 +++---- docs/guide/widgets.md | 41 ++++++++++++++++++++++++ docs/images/scroll_view.excalidraw.svg | 6 ++-- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/docs/examples/guide/widgets/checker04.py b/docs/examples/guide/widgets/checker04.py index 54f2d7522..1243a3e6d 100644 --- a/docs/examples/guide/widgets/checker04.py +++ b/docs/examples/guide/widgets/checker04.py @@ -26,11 +26,11 @@ class CheckerBoard(ScrollView): background: #004578; } CheckerBoard > .checkerboard--cursor-square { - background: darkred + background: darkred; } """ - cursor_square: var[Offset | None] = var(None) + cursor_square = var(Offset(0, 0)) def __init__(self, board_size: int) -> None: super().__init__() @@ -44,7 +44,7 @@ class CheckerBoard(ScrollView): self.cursor_square = Offset(mouse_position.x // 8, mouse_position.y // 4) def watch_cursor_square( - self, previous_square: Offset | None, cursor_square: Offset | None + self, previous_square: Offset, cursor_square: Offset ) -> None: """Called when the cursor square changes.""" @@ -57,12 +57,10 @@ class CheckerBoard(ScrollView): return region # Refresh the previous cursor square - if previous_square is not None: - self.refresh(get_square_region(previous_square)) + self.refresh(get_square_region(previous_square)) # Refresh the new cursor square - if cursor_square is not None: - self.refresh(get_square_region(cursor_square)) + self.refresh(get_square_region(cursor_square)) def render_line(self, y: int) -> Strip: """Render a line of the widget. y is relative to the top of the widget.""" diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 96b9cbff1..9470d123f 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -339,3 +339,44 @@ We also need to compensate for the position of the horizontal scrollbar. This is
--8<-- "docs/images/scroll_view.excalidraw.svg"
+ +### Region updates + +When you call the [refresh][textual.widget.Widget.refresh] method it will refresh the entire widget. +The Line API makes it possible to refresh parts of a widget, as small as a single character. +Refreshing smaller regions makes updates more efficient, and keeps your widget feeling responsive. + +To demonstrate this we will update the checkerboard to highlight the square under the mouse pointer. +Here's the code: + +=== "checker04.py" + + ```python title="checker04.py" hl_lines="18 28-30 33 41-44 46-63 74 81-92" + --8<-- "docs/examples/guide/widgets/checker04.py" + ``` + +=== "Output" + + ```{.textual path="docs/examples/guide/widgets/checker04.py"} + ``` + +We've added a style to the checkerboard which is the color of the highlighted square, with a default of "darkred". We will need this when we come to render the highlighted square. + +We've also added a reactive variable called `cursor_square` which will hold the coordinate of the square underneath the mouse. Note that we have used [var][textual.reactive.var] which gives as reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor. + +The `on_mouse_move` handler takes the mouse coordinates from the [MouseMove][textual.events.MouseMove] object and calculates the coordinate of the square underneath the mouse. There's a little math here, so let's break it down. + +- The event contains the coordinates of the mouse relative to the top left of the widget, but we need the coordinate relative to the top left of board which depends on the position of the scrollbars. +We can make this conversion by adding `event.offset` to `self.scroll_offset`. +- Once we have the board coordinate underneath the mouse we divide the x coordinate by 8 and divide the y coordinate by 4 to give us the coordinate of a square. + +If the cursor square coordinate calculated in `on_mouse_move` changes, Textual will call `watch_cursor_square` with the previous coordinate and new coordinate of the square. This method works out the regions of the widget to update and essentially does the reverse of the steps we took to go from mouse coordinates to square coordinates. +The `get_square_region` function calculates a [Region][textual.geometry.Region] object for each square and uses them as a positional argument in a call to [refresh][textual.widget.Widget.refresh]. Passing Regions to `refresh` tells Textual to update only the cells underneath those regions, and not the entire region. + +!!! note + + Textual is smart about performing updates. If you refresh multiple regions (even if they overlap), Textual will combine them in to as few non-overlapping regions as possible. + +The final step is to update the `render_line` method to use the cursor style when rendering the square underneath the mouse. + +You should find that if you move the mouse over the widget now, it will highlight the square underneath the mouse pointer in red. diff --git a/docs/images/scroll_view.excalidraw.svg b/docs/images/scroll_view.excalidraw.svg index 0d3ba66a8..4dfc06120 100644 --- a/docs/images/scroll_view.excalidraw.svg +++ b/docs/images/scroll_view.excalidraw.svg @@ -1,6 +1,6 @@ - + - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1da1NcIsuy/b5/xcScj3dTp96VdVwibtzw/cQ3Prj3hMGjXHUwMDExtFx1MDAwNYRG0Vx1MDAxM/u/30x0S/NoXHUwMDA0RaZcdTAwMDdnwlx1MDAxOWnAomtV5lpZmVn/+ePHj5/RczP4+a9cdTAwMWY/g26pXHUwMDEw1sqtwtPPP+nxx6DVrjXqeEn2fm43Oq1S75nVKGq2//XPf95cdTAwMTdad0HUXGZcdTAwMGKlgD3W2p1C2I465VqDlVx1MDAxYff/rEXBfft/6PtB4T7472bjvlx1MDAxY7VY/5dkgnItarRef1dcdTAwMTBcdTAwMDb3QT1q47v/L/7848d/et9jo2tcdTAwMDWlqFC/XHSD3lx1MDAwYnqX+lx1MDAwM7RCXGY/etCo91x1MDAwNmtcdTAwMDRcYrDGqPcn1Nrr+OuioIxXKzjkoH+FXHUwMDFl+nm6X1x1MDAxMfXgtntcXFrNtM787bZpXm/1f2ulXHUwMDE2hqfRc/h6J1xupWqnXHUwMDE1XHUwMDFiUztqNe6Ci1o5quJ1MfT4++vaXHK8XHT9V7VcdTAwMWGdm2o9aLdcdTAwMDde02hcdTAwMTZKtehcdTAwMTlcdTAwMWZT/P3B13vwr1x1MDAxZv1HuvhTXHUwMDA2JDNSXGIvXHUwMDE0SK3k+0V6tVx1MDAwNmDaOKe4kpJ7I9TQsNZcdTAwMWEhTlx1MDAwNFx1MDAwZetcdTAwMWa899VcdTAwMWZYsVC6u8HR1cv950hcdMXA9p/z9PZhrWVcXDtntNRcdTAwMDYs9/r9XHUwMDE51aB2U43wKU4yUMZya7nkXHUwMDAw0Fx1MDAxZmc76M2HXHUwMDE0Smu87t37XHUwMDE1+vXNnXJcdTAwMGZcdTAwMWH/jt+xevntjtU7YdhcdTAwMWYxXdiIwan/mk6zXFx4nXZhcZjaWqO89u/Xw1r9bvjtwkbpro+U3qN//flcdIQ6gCSEXG6rXHUwMDA1N8JyNzVE13OVu9vuysFDVl1cdTAwMWOfVs5cdTAwMWXzufWVlEPUWCaMdlZp6Vx1MDAxNN5cdTAwMGVvR0GqlDdKIE6VXHUwMDEzNrUgxeFr77k1S4ZR62RcdTAwMTJGvZPOOyfF1Fx1MDAxMFxyzrZ38e5cdTAwMTWLZX1ylN2Bvb2jO51yiGaEYlxceDBeOHRcdTAwMWFcdTAwMDBiXHUwMDEwo8Yqpi1wvCQk6OFxpVx1MDAwN6HCKFx1MDAwNfhHLFx1MDAxYkKtT0Ko4Fx1MDAxYX1cdTAwMDc6uOlcdTAwMWT95WHxYO/KQLFxdHxfuHzcRn7T/rVcdTAwMTBcdTAwMTX8QzOqmTPIZyRY4dFzuFx1MDAwMYRa5Vx1MDAxOZJcdTAwMDDv0UJxo41MLUSlcE7jUJdcdTAwMGWikExFJTo9iHObj1x1MDAxMHqfyVx1MDAxN9RmaSV3mq/eXHUwMDA169Wjja1QpN2IgmDIQjUoK1x1MDAwMVdlzEz2XHUwMDEwqjlDXHUwMDBiqo1cdTAwMTZcdTAwMWGXqzOpRahwOF1cdTAwMDZ9wNJxUZPMRVx1MDAwMY1cdTAwMDbeL+Gnxqh3XHUwMDA1a8LG7W2nlFx1MDAxM9dhp9w4dFx1MDAwNynHqJCGeYuk21x0hICPXHSiVzdvkIpcIlx1MDAxZlx1MDAwN6k0V7H5T1x1MDAxZER7r1x1MDAxNchXXHUwMDE2XHUwMDA1UcNcdTAwMTdcdTAwMDNRp1x1MDAxMiFquHWOm1x1MDAxObho62W3uuI2XHUwMDBljNjeVkFm/WXzqHqedkePc4ueXHUwMDFlOZyTXHUwMDA2/zUwKOpcdTAwMTG5TFx1MDAwMmp5a6X2UqaZjHJcdTAwMTDgOF8+kNpEkHortOFuejO6ooo+ut95gUZ1+3QjdyFcbuf1tEv6jGecK1xcjrhcdTAwMTjR43tcdTAwMDdDXHUwMDEwNUxJnFxyp1xyisf0XHUwMDAyVDiLmlx1MDAxZd9nyVx1MDAwMGpcdTAwMWRPXHUwMDAyqORCXGJcdTAwMDAuzdRcYl31uuGj6G612L6T97Ys2lf1ZspcdTAwMTEqvGFCO8GFVlx1MDAxNGSTg55eg0VscPyHW4n+Xuj0glQhfsDhq5dcZqSoXHUwMDE0kkDqrFx1MDAwMudR006N0b2wW12vqnw24kG4YZ+ew2b3LOVcdTAwMThVSjFkmtygI+dg5XDwXHUwMDFlIYomXHUwMDE2QeG4wmWb3rgorjFkI9yohVx1MDAwNUZcdTAwMTfl6GWio/fKI7nhfHqImruNsFiuPG6ePlx1MDAxYV7OX4bX9Y0w5Vx1MDAxMJVWM1xcirhcdTAwMTiFxvvOzVx1MDAwMESNXHUwMDE1XGZcdTAwMTC6WlJYSqbYhlx1MDAxYeuExPlaPoAmRu6VJf+mZ7Cha3f6Klx1MDAxMrflm9xBqXX+1FK5aqGQdrVERlRrb71CXHUwMDA0XGLBXHUwMDA3mShcdTAwMDWdhLTgaG9UgE0xRJ1Sgr4tXHUwMDFkRGPbm8NiSeJQXHUwMDE0LsxcdTAwMTmM6O3V9u6+y1x1MDAxZK7ooCF3XHUwMDBmz8+beZd2jKIoZFxiXHUwMDBlUNYjIVVqXHUwMDA0pJKhVkJcdTAwMDIgUTnFWFHqMIqLXGaQqCxuk35RVFQkXHUwMDBieuO1XHUwMDE2XHUwMDFh3PRRJ7m94k7C3Vr+ca15e3Mo7yv8Np9yR6+5YlaiLvRee4G6fcjRI1xykFxiXHKuwEu0temFqDXgtDNu2cwoqMRcdTAwMWRQdIHOOWWnt6JcdTAwMDfR9WNN33XPm6tcdTAwMGb5leg5e3pS3fhcclx1MDAxMKqNoHwmg35SqkFFj/yOIW5p+oFTvklqIUqpMM5cdL10XGLliVxcVChtaNOFT79Hf7X/tFWsdithXHUwMDA3Lnbq/mKtUT+8SrujR6bJrFx1MDAwM4lKSVgwUrshjDrGJeDMIHgokSi1XHUwMDE4ldwrNKPo/JZcZqRxXGJcdTAwMGVHRilcdTAwMWNMSnZ6O1x1MDAxYV695Fx1MDAwZm3JXHUwMDFj39unenS4d3/bujxOuVx1MDAxZM1cdTAwMDBKXCLLlVx1MDAwMKUoZjNcdTAwMDRRz5lcdTAwMTNcdTAwMTZcci0uV1x0Or2CSVx1MDAxYiW9VVotXHUwMDE5QsHoJIRcbpDOaS9nSFx1MDAxOL15Km2e19pcdTAwMDKuc8+7taq2p/mNX1x1MDAxY1x1MDAxN51cItXJoVx1MDAxZVJIylx1MDAxMYOgIY6PV4xcbmRcdTAwMDLW49RcYlT8MVx1MDAxYpU2jOJFXHUwMDAwpWHpNkCTMUrcxlo7/e5S0K7o0tnZntlrrjVtPtupblx1MDAxNLfTbkNcdTAwMDVnyDQ9WCFcciXcxXxcdTAwMDa9XHUwMDAxIHhQRqHYR8mM2EkvXHUwMDE5XHUwMDA1XHUwMDBijvtlo6JcdTAwMGWSs0gkTlx0cjA1vZ73PKxGneKWXHUwMDBi9ss1nr0sdTL7jZQjXHUwMDE0TShcbkLpXHJyUePju730cq8581riWtWSq1x1MDAxNKeQoI5HKqKWL25cdTAwMWZcdTAwMGInjdBQXHScWzmDXHQtNqriprW7d/+yenR/89jcPbzcyKZcdTAwMWOgXHUwMDE5XHUwMDA3zKGrQP9ccsg00ZBcdTAwMGUhVDHS+OCk8Vx1MDAxNmR6s0UlRzLNpeBLR0RV4v685oaD4jPY0NPsVebs7vJg57F01dk5XGbCp6fDx5RDVEg0ouhcdTAwMWNcdTAwMWSXiE87tPmJnlx1MDAxM5VcdTAwMTIqfVx1MDAxNFwi2juR3ox7J53xS2hE41x1MDAxZnV4d15q9G3KTlx1MDAwZtCNXHUwMDFkeLo536tnXHUwMDFmg+PcyupZ+bpTT3tIlFx1MDAwMEqln1x1MDAxZVxykKKakFx1MDAwMYBcIvs0jCuDRtZcdTAwMTht40l6aUOosPTH+OVcdTAwMTPzNpGIXHUwMDFhb7RA9j01Qlx1MDAxZreeosuTnZvsej5Y27292i1VXHUwMDBilylHaMYxjb5Ro7sw1jo/lMuMXHUwMDEwdVxmNH5xZHqopdJrRNH+S8f50mXhOZm4r1x1MDAwNFxcWSPFXGZcdTAwMTX0fv9O3EeZ7f3mw1G4tZepd831UdqjTcI7KqHn6OVcdTAwMTGqcdbzXHUwMDFhbJJMXHUwMDBi06u5621ipFx1MDAxN6FcdTAwMDLXXHUwMDEw18tcdTAwMTdcdTAwMTH1yVx1MDAxOSRegzbe++n9fL5WXHUwMDE1QSOzWatnczWT3d5q+KdfLOanSXKyXGZcdTAwMDFIVFx1MDAxNP29XHUwMDFm3lZCiCovgMAjadcmtVx1MDAxMLVWXHUwMDFhVHSwdEbUJ1x1MDAxYVGPQklRjcTUXGK9PD8uNfxx+eaidpI7M6b10lq9SLmfl5Z2NlGva42kXHUwMDE0yeZgxFx1MDAxZfCyRiGPcl9cdTAwMTmZZlx1MDAxYiqpwoxcYvOSITTeXHUwMDEyYKSAXHUwMDFlNS66PTu9n89d7ja3ajtbJ9nWzr45i04vzttp75SjlGEgONJQZKRcdTAwMWH8oFjyWjCgzFx1MDAxMqnI1ac4JCqUXHUwMDEy6Pbs4rqQ/PokPEfJkVx1MDAxNmZItverl/Xzx+p1VJSrsF15Ka5cdTAwMDdPXHUwMDBmKUeo8MBQyjvuvbFGXGJlhyCqXHUwMDE5uk+Jt4JcdTAwMGIlVXrlvMd15rhbOlwiXHUwMDFhN1x1MDAxYSNldVx1MDAxZeeNXHUwMDFimN6IXHUwMDFlXHUwMDFk7LlORl2Is+fb+pPblFFxO0g5RDVHeKBcdTAwMTDG+ZVcXOEkXHUwMDBmx0Q9s1x1MDAwNkU9UlRLe6PphaixWnprli2T2SeniaI6VJTEO71cdTAwMTHdKIb11sNhcVx1MDAxM8Tu6snd1t3TeniSeoSSm0cmylx1MDAxZJohwVx1MDAwN21oLyaKK1x1MDAxNc2r9lap9KaOXGKJy1x1MDAwN7XD0lx1MDAxOVGf3LJcdTAwMTGZl6e9lult6Lrbe8zXNtZfwpuwWatcdTAwMGLYutm6TzlC0Xoyjnpd4uyjXHUwMDFi5+CGIUo7o15cdGNwduI8L3VcdTAwMTjVVJ0q49ssy4FRpVx1MDAxMneWkHvR1OhcdTAwMTlcdTAwMDR99bGws7lcdTAwMGZh96q4vul2XHUwMDBicLJcdTAwMTckZThccoFtXHUwMDEwosPJmO+vKlx1MDAxN9rVYFx1MDAwNoy6XHUwMDBm4/YgKeAkjXZcdTAwMTR3XHUwMDFhjttTbbJcdTAwMTVUUumMkVx1MDAwM7tcdTAwMTifwGjUKtTbzUJcdTAwMGIxMIpTYzSz1oFxXHUwMDAyvDKxOv53nFwiQlmvo5QxSJ7FuJZjVGfmpJ5cdTAwMWZO3y70gVx1MDAxNZvvhrvaPrySK83z0/Z5pdbydZvtN+9cdTAwMWFAYaHVajz9fL/y15+T3jd7uX/LXHUwMDBiXHUwMDE39qB6WuJHzWJcdTAwMTTumZ3p3vftf8mri3J+wdiFrC5cdTAwMDEx2jhcdTAwMWOMXHUwMDEwTkol4onnXHUwMDFmNlC5rlx1MDAxNKKbo8PTRqaqiqXN09MtnVx1MDAxNIz43PL6jl5+2oIxXHUwMDE2NMo844fqXHUwMDAxXHUwMDE1MKWF8r1Cpngs6jNcdTAwMGWgUqmMriqlgVx1MDAxOUr+kqQzQcDoqkLPxDxVMqDYtFx1MDAxMsyYQISXVHNrXHUwMDE2tKouy+dr8qHycFA99NY2Vu1R9mwu6EfB4rXQseLgb+U/k5pcdTAwMDc5IHFcdTAwMGbTp3650vZevVSMoue9R7ArL+V93emmfTvDXHUwMDE4plx1MDAxNDWpRHBro61cdTAwMWIk6VZcdTAwMGKmXHUwMDFjte9G/jtQ0p02XHUwMDAy5ITSXFwv3Z6wh0SIWrxXg61cdTAwMTE/QujOSu7xKbzL6fzV/vrmak6vupNS2lx1MDAxMYpcdTAwMDaBSWucorZcdTAwMTZcXEg9XHUwMDAyUC6s5EhcdTAwMDSpnjvNXGadci+8WTqG7lVcIoUwXpLCXHUwMDE300NcdTAwMTTOt7Ln3Vx1MDAwN7e9una696gvQ7W2/YtbsE2zJyyZQ9vjkX57591wQ2AtmaJiQ/Dou9Pcm8XTXHUwMDBlPjqBZbOiXCLeXHUwMDAxd3hX2FxugPj1jyDafV65XHUwMDBiXHUwMDBi4cZu/qSbz5/7YrhcdTAwMGatz5Dc4Vxmq28luZzjPUd+6Vxy5XJcdTAwMGZ5eWS5XHUwMDBlXHUwMDA1JFx1MDAwMtRS8sJ3kFxcx4zGMShqXHUwMDAwZ/RcdTAwMTjpKIhpK4GKXHUwMDAzqEuMiFfqvKUsSOBK6Tnutk1cIrlSdp/32m1x+qTrYvf5aNef1trTkdyJ0vH7yDNJRyVNrFx1MDAwZtFcdTAwMTfXVFx1MDAxMIa1Znv8ilJcIrmBsfZcdTAwMWW4nyXRYi2z/bJzepBT3SPVvs03N59cdTAwMGXDpODhxDW1OKNvkXjQXHUwMDExXHUwMDFhhvqqxrtcdTAwMTa+mnyDtFx1MDAwNDVcdTAwMTlqNvfF4vJKweCkjq4pko24ZPH1XHUwMDFhmVH8sI73NVx1MDAwNehcdTAwMTOk69Vm9trVq+ElJbzUvb6h89t8+aY1NVx1MDAwNfa1NzG59p3YTz5cdTAwMDLBeGOUniVu/sxrV9XHXSfE01634nObnc6lTDf0XHUwMDFkmmoqSrdcYm2qtlx1MDAxOe4uj4SdWimCV4iuL+a6V1xuRXRcdTAwMWLfg37qbWnNPGORXHUwMDBiozuT4KllYqY7XG55ZOTxhidcdTAwMWZ2nO1cdTAwMWWdn1xc3rTr2Vb9zJrM/Y6BzXTD01uGXHUwMDA2U0nuPNdeXHUwMDBlgVMxwFx1MDAwNSxcdTAwMWMlwVx1MDAxOfe19klCXHUwMDE2XHUwMDAxxpDxeYBTalxyQs4zpJdcdTAwMGUu7pNcdTAwMGbnXHUwMDEwyminZypcYn7Y2axtXHUwMDEzcXBXL/XOirx7Upu/uMHXXHUwMDE0dNxcIlx1MDAwNr3qKUZj4seS9XaE6Fx1MDAxNC7KVUVcdTAwMTGHbi3F7ZOEkJb2epauf1K8XHUwMDFm0MimI46EXHUwMDBiN0MpxqZ7qDm/W2msXHUwMDFkrF1GK7X7YG/9XHUwMDE3N+2eLqbhlTGAupFcIr+DPp5601CNXHUwMDEw6jRKtUpzjiaOTjngv6OTn9xYPrlnN5F6QPc3Q1x1MDAxNvFxtqP2Xe5hnW+s7u095lZlYkFbSry8cVxmuSVaSXS0YOKHi/bS34xEI2pcdTAwMWR1i6QjN7+Ez1JQ1uXCKD5cdTAwMTUyXeW4clZah4slXHUwMDE29+uHNahdI3ikXHUwMDFiXHUwMDE0XjHW2mGE9qJcdTAwMWVOiflcdTAwMDXd3i6MlWB201x1MDAxY5xl1ctcdTAwMGVcbteN64xZ27o+OJ6LXHUwMDA0M1xcgOYm5ry/N3cpXHUwMDE5/lxiXHUwMDA2UGZcdTAwMTaSXHUwMDFiZFxu4uRcdTAwMTai5sWzWt3oVvaz66lnXHUwMDExyCCYk9bQ8Uh0XHUwMDA0iFx1MDAxY23NxLmlY+gsTotMb4c76KWU2KWz0GBcdTAwMTNTl0BcIren7phTQ/QqbK+oze76+Vx1MDAxNc9cdTAwMTVcdTAwMGWti1xumysq3Vx1MDAxNlx1MDAxYWUms1x1MDAxMkWWXHUwMDE2XHUwMDBlXHUwMDE1l4WhXGZlo5mnXHUwMDFlxpwyXHUwMDE34GtKTJdsUFx1MDAxOVx1MDAxMyZcdTAwMTDeM4WKV3hJPaL4mOxcbiFcdTAwMTlCV1rq5lx1MDAwNtrH2/C918rhJzBmXHUwMDAx5yTSXHUwMDFlJnWlXlx1MDAxMM2ViVJMO4nuk8/QXHUwMDBl/Oqhe3B8tVLYjfZW99TFXHJf21x1MDAxMOnfXFyWilFcIrKhPHmQhlx1MDAwZndcdEVcdTAwMDSDVkohkzDxM/vSZkNcdTAwMDXFOpxbvrZccrhukzCKRkNcdTAwMTL9miFBLVqrXHUwMDFkVGvtqlup6M1G5rbmzoNi2kGKPp6qjdCLK6c1QnFcdTAwMTiknkluXHUwMDExq1x1MDAxNC6K979LXHUwMDFiSHGmXHUwMDA0UCrrkmFcdTAwMTRsYlx1MDAwNoTstb6JXHUwMDE3t3yYorx+5neei2vXXHUwMDA1ffTSvCxcdTAwMDTHpeukQ7tT4uhcdTAwMTVYJiVyUaD84/jhur2Qq7Gokjxcblx1MDAxY8Wt4v5r+EzSYkIxTYmz1lCCpPWxvNa+p/eajlx1MDAxYXNcdTAwMDJcdTAwMTmJljYukPtp9I7aNsyx2fLbhbFibDtfs+FZ/SVqXHUwMDE3+NXa8cHGXfelMTcxZlFzLqqkObnfOKWpcWvMXGZUV2yWSt3cYe14k992/MvmmdxIOio0NStcdTAwMDCYsFJcdTAwMGKiu9xcdTAwMGI7SHUtSjXllPROOan813L0k7kudZMmmlx1MDAwYlxidCpcdTAwMDdcdTAwMTljo0FSM0pOJZK9ipZcdTAwMTGuS92mqX3F91fjXHUwMDEx15U4XHUwMDA2mFx1MDAxN0SjoFx1MDAxYo1Dp0guuLcoxJw0M+RQXHUwMDFll7NcdTAwMGaHNydmq3nz1L3PRrdB2z2nu4JEo1x1MDAwNqLKXHUwMDEwiSSCmrTYwexcdTAwMWanqdaZXHUwMDFiytn48knLY7N/pFx1MDAxOYNENVx1MDAxMlxiQ5uNLntRgbDvKlxyOd88tqVMqVx1MDAxMOZbqttdy+fyXHUwMDFkeTqnXHUwMDFjXHUwMDA3a4FcdTAwMWHMzbBg+sBs1KPT2kuPk8DAo5uF+1r4PFx1MDAwMK7eSsJcdTAwMDE+1lpRp1x1MDAxMF638YXsbeZi099cdTAwMGVwXHUwMDAwvXc0XHUwMDAzL11cdGs3tPB+hkFlcEVGNbwx75ejRmxcdTAwMWKkhEMp4Nu1dsrDXHUwMDFmqdGq3dTqhfBswrAmWobXXHUwMDFiPcY0qGRcdTAwMDVsUPRJZ2do8D5cdTAwMTlRvyAx8EPLgHqXIaq0RFx1MDAwNoe0x1x1MDAwZtU/OmVcdTAwMThcdTAwMWRcdTAwMTVcdTAwMDKoK5A1qa+FaMYnXHUwMDA2Mo5uXHUwMDExXHRcdTAwMTeKbE6pSGO2yqXWzDvjKTlQXHUwMDAzWDFiOchjOVx1MDAwN8BcdTAwMTdRQz57fVbstlx1MDAxNVrRaq1ertVv8GLfevxcZl5/9c5cdTAwMTQ+hj5cXKH5Snkpzi3wznhUO7FccqHeyi516INkOEPeTWeaeeWIXHUwMDFmg3x70rtcdTAwMTn7XHUwMDE51MtcdTAwMWZcdTAwMGZqcunk+6CUYF5cdTAwMGLA2TBC+T4vjFx1MDAwZkkwIZRGLWtcdTAwMTRSdcpcdTAwMTRcdTAwMWFcdTAwMTlSWGhHa437+1qEt/6oUatHw7e4dy9XaGFXg8KIycCPXHUwMDE0vzZsXHUwMDAxmvSOgy6i/79cdTAwMWb9VdL74f3///5z7LMzySDuXVx1MDAxZcVv/1x1MDAxZP+I/zuz+Vx1MDAxMsl7gJRXIES8n/xH5muy40qn+fJcZjWlprx1izLbXGbtgCAp59zRXqjzgsM3XHUwMDE073FmkDUhrzLcoHBcdTAwMTRuTGKz4pJJS6ebW0F9OmDUfFx0OqtcdTAwMWVF1OJaYMyySTd38/W3IeBcZsCTm/WANpUqmEfMXHQwXHUwMDE0j6jZXHUwMDA1XHUwMDAwiXbrJ1x1MDAxYq/BT/FcdTAwMWLZkGQg0dcohGa0IEnSKN7mdSRcdTAwMTlcdTAwMGIlXHUwMDFhmvJcdTAwMTm0USP3fHHvXHUwMDFhQbOUvWxcdTAwMWbqrjeZ809lXHUwMDExLE5cdTAwMWKBZ85cdTAwMWJUzdqgbOZ8MIuAKlx1MDAxZZmmykdcdTAwMTT4SukvxlbHiyM9lTiiNFtnYI7ng7xDaL717Vx1MDAxM8XR9Vx1MDAwM1L8+7Xyzfo1+svts6PDnVJ1buLIU6erXyOOXmczbdrodVSf41x1MDAxNtIkXHUwMDE3RyPRpqyq6bnFZDylkVsojayOTrKmPHdDnVx1MDAxMIYsg5RcZijjhfr/qa+eKzTWMlxiblx1MDAxOeCvRiaNXGLXmo8xXHUwMDE0glkklVx1MDAxNlx1MDAxZCN1/Fx1MDAwNFx1MDAxZlx1MDAwYp78XHUwMDFkVXHguFrEtuD3XHUwMDBio8lcdTAwMGXmnTR4Jr1z0jrR61bIx4pcdTAwMTDOOEXDKHeGU1xuKzrdydzii8JIXHUwMDAyXHUwMDEzkvJcdTAwMGLQzzhFp1x1MDAwMY2XRtxz2shQlFx1MDAxN2Hsby6NkiFMX5lR9M7Ia5KVkUtsVO1cZkphcGL6XHKJyV4rjdbLaUat/7g0mpuB3npvxssxXHUwMDA3XFxcdTAwMTmr8Vx1MDAwM1x088VcdTAwMDK1scbLoMpcdTAwMDdLMFx1MDAwNlxcYvFGXHUwMDFhcePluFx1MDAwNuetoj6VOJBcdTAwMTHjhbBcdTAwMTB+UUdIf5o/zMt8xYxcdTAwMTOdriyRmSrPgbpcdTAwMWXEYztv4Vx1MDAxZkOnNtGhTFx1MDAwMqRQ2n9gv35bbZRJxtLr5Vx1MDAxMVx1MDAxOM1oRVx1MDAxMtVRcvNccrQggJiZ4cyQo+rWUzfMZ5+fX9aMWslqU6+s/0oj4j/sPKaYRNugnNCSmuPBUCtxo/DOa6Q+YD1Y578hvjKtOOpcdTAwMWRY4+epjpJ3LSVcdTAwMTfcwSxF1l/TXHUwMDE5qCQqrF1qNcLwulGptINU7MGMXHUwMDE51ec8tUruQCY5rjCNXHUwMDBlavpChslcdTAwMTXGaXTVXHUwMDE5Z5hQRijK01x1MDAxNsrqwdRcdTAwMDHteW/z1oLUSGrENywyIVx1MDAwNdOW2kOgXHTt9VxmXHUwMDFl46qtYo6OonZcdTAwMGX9kVWjPcgknXfi9GKO5cH7IO3nNkKHfPWwV/vYi0/uVVx1MDAxMPfimjpagESqidxcdTAwMTZvT1xch7z7cOWpM1xmd8oohbrg96b7iUjqXVx1MDAxZMHQnPy0SDYhXHUwMDFhx6GUnKFcdTAwMTTq5OLusH7zeF677VxcZVx1MDAwYkadlcVumG4/LYVjSmo6hJvjXz90bpK1llx0R2RcdTAwMDX1p+P8+zqEitjin+SuUVx1MDAxMnrkXHUwMDBic0yof0fS4ttcdTAwMTXSWVSLI1x1MDAwM69cdTAwMWX3OVx1MDAxNVx1MDAxNODvsXzO8Us9oW+YXHUwMDAzXHUwMDA3Wk5cdTAwMWZgnDzHqVxctVS+JdA90KG64NRQ0ixIRl3ikVx1MDAwMCtJNuz71iyyeFx1MDAwZd6j4DRcdTAwMGVoK2SM93eU32tcdTAwMDVcdTAwMWRhKfGvXHUwMDFiSVx1MDAxYlx1MDAxNFx1MDAwZXmxp4rLXHUwMDA1MPAvLLoplfpkJ/AjvoUpPWhqXHUwMDBlj5ZXcydjXHUwMDFl729cdTAwMWavXHUwMDE53jlAfY5cdTAwMDJWXHUwMDE56z6ZgDG5ue7AmEBcboppXG5cXEdIy5xcdTAwMWRcdTAwMWSTY9yjx0D00amCyrrfm3ckYpi+MqPwnVx1MDAxM/GQMnn7VKBittrPcFxuiTzKtNbD41x1MDAxNd9ccnNbudr9bl5cdTAwMDCk24YpYPhcdLmm1sXUXHJg6JhcdTAwMWPnXHUwMDE4OXque7Hfr5X4TTZiMSoxkXj0XHUwMDBlRpvjbsgk3vG9pdZcdTAwMTStXFw47+imiHd0P8k7dKxX2/CilVx1MDAwNmVcdTAwMTh+n554TJ7kNC5aScFUw42WXHUwMDAyXHUwMDE1XHUwMDE5wNCapbPU0U2hwtbKf/HIi4lrVlx1MDAxOdpg7Vx1MDAxZIntpIiXXCL0eVx1MDAwN/pO6o/EXHTz1nM1skEgnCCnOs+k8Y9cdTAwMTbdd6ZOTXZcdTAwMDE/XHUwMDA2ty+N94pcdTAwMDKf6OC1pLlcdTAwMWFx857R0V9KoS9CTT66lThcdTAwMTXvmNxcdTAwMWIkNibqNyC5kFx1MDAwMII6p+uR4YxuUPxOJCNcdTAwMTGv9JVcdTAwMTmF6pw4hpqUosV7jVx1MDAxZvz0u1x1MDAxMFx1MDAxYnZrrds5z8NcdTAwMTU/vburVc15mEs3x5BcdTAwMWFcdTAwMThcdTAwMDWNXHUwMDE0cmjq56JcdTAwMDZcdTAwMTMxPFx1MDAxNaZSQqOToMxcdTAwMTerXHUwMDBiJ3NcZlx1MDAxOFx1MDAxM9yIn/D7VkAl8Ylaz/Fg3UlcdTAwMWPjOytcYmUvM2LxXHUwMDFj48d//V/9dXthcmaVXHUwMDFkfItF0Y5xw/tcdTAwMWNcdTAwMTORyUlcbsiqXHUwMDA1R188fWn7ZCSkcWXjckX14KhpunA8nqree7mhsjTtKDxuuVx1MDAxNMPFnHMkXCKWec1cdTAwMDWdr+SohL5/XHUwMDAz+kSE07neXG7QyjurjLCjlcOKXHUwMDFhTlx1MDAxMn1cXFx1MDAwMFx1MDAxMfnCypySiEz2XHUwMDEzQ0TEgpV0hCO3qPJcXL92+t3xO0b1XHUwMDFlkvZcdTAwMDU0UIX455jI5NL4wUFR0p7GgTlJJ7v50bCMYeg9fS/tg3tcdTAwMDD9e3OTJFxi01dmXHUwMDE0vUnc5I+33/Cz0GyeRoi599lANNfKbya9/zF/PtaCp9XxXHUwMDFijbTX+MfbXHIl81x1MDAxM9CH/c9ff/z1/1x1MDAxNXZx8iJ9 + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1da1NcIsuy/b5/xcTcj3fTp7Kyslx1MDAxZSfixFxy309866j3njCQh6AtIKCiJ/Z/v5nollx1MDAwNmxcdTAwMDRHnVx1MDAxZZiImZFcdTAwMDYsulZlrpWVmfWfP759+955aJa///Pb93K3WIhrpVbh/vuf8vxdudWuNep8Sfd+bjduW8XeK6udTrP9z3/847rQuip3mnGhWI7uau3bQtzu3JZqjajYuP5HrVO+bv+P/L1VuC7/q9m4LnVaUf+X5MqlWqfRevpd5bh8Xa532vzp/8s/f/v2n97fidG1ysVOoX5cdTAwMTGXe2/oXepcdTAwMGbQXHUwMDAyXGY/u9Wo91x1MDAwNkvgwVtcInx5Qa29yL+uUy7x1VxuXHUwMDBmudy/XCJPfd/frEC9fNndLc7nWlx1MDAwN+FylZpnK/3fWqnF8X7nIX66XHUwMDEzhWL1tpVcdTAwMThTu9NqXFyVf9RKnSpfh6HnX97XbvBN6L+r1bi9qNbL7fbAe1x1MDAxYc1CsdZ54OdQvTz5dFx1MDAwZv75rf9Ml3/KeVx1MDAxZJFcdTAwMDZcYoBeXHUwMDFi1C9cdTAwMTfl3cb7yJBzqFBrXHUwMDE1XGJwaFhcdTAwMGKNmCeCh/VfqvfoXHUwMDBm7LxQvLrg0dVL/ddo7c/Ltv+a++cva22kjHNktCFvVTAvr6iWa1x1MDAxN9VcdTAwMGW/xOnII1llrdLKe99cdTAwMWZnu9ybXHUwMDBmXHJoXGZfXHUwMDBm7uWK/PrmWqlcdTAwMDeNfyfvWL30fMfqt3HcXHUwMDFmsVxcWErAqf+e22ap8DTtYHmYxlrCYMLL9bhWv1x1MDAxYf64uFG86iOl9+xff75cdTAwMDOhzvs0hII1oFxirHJcdTAwMTNDdPGwcnXZndu6yeOP3f3Kwd3p4eJcXMYhSjZcdTAwMDIyzqLRXHUwMDBl+XZcdTAwMDQ7XG5SxEBcYoxTdGAzXHUwMDBiUlx1MDAxZb5cdEFZmjGMWqfTMFx1MDAxYZx2wTlcclx1MDAxM0O0fLC6znfv/Lxk9nbya35jY+fKZFx1MDAxY6I5wEhB8Fx1MDAxNMCx0/BcdTAwMWVcdTAwMDYxSlx1MDAxNiNjveJLoL1cdTAwMTlcdTAwMWVXdlx1MDAxMFxuhOj5XHUwMDBmzFx1MDAxYUJtSEMoKMO+g1x1MDAxZNzkjv54+3xr44T8eWNn97pwfLfK/Kb9ayFcbupNM2pcIkfMZ7S3XHUwMDEw2HO4XHUwMDAxhFpcZlx1MDAxMZOAXHUwMDEw2EIpMqQzXHUwMDBiUVxyzlx1MDAxOVx1MDAxZerMQdSnU1HNTs8nuc1bXGK9zp1cdTAwMTZwuTh3uH9avSovVneWVmLIulx1MDAxMfVcdTAwMTAxXHUwMDBiNVx1MDAxZa32vCpcdTAwMTNmsodQo1witqCGXGZcdTAwMThero4yi1BwPF3EPmDmuCilc1HPRoPvXHUwMDE3hIkxXHUwMDFhXFzBUty4vLwtXHUwMDFlwll8W2psu62MY1x1MDAxNDRFwTLpdsBcdTAwMTBcYlx0QfTk5ompKPNxr9EoTMx/5iDaey8wX/kqiJL6XHUwMDFhiDpMhSgp65yiKbho63G9OueWtlxiVlexnFt8XFzeqVx1MDAxZWXd0fPcsqdnXHUwMDBl5zTxv+RcdTAwMDdFPSM30p61vLXaXHUwMDA0rbNMRpVcdTAwMDfvlJo9kNpUkFx1MDAwNlx1MDAwYoaUm9yMzuF56Fxcrz36RnV1f+nwXHUwMDA3XHUwMDE0jupZl/S5XHUwMDEwKYW8XHUwMDFjeTGyx1x1MDAwZs5cdTAwMGZBlFwi1DxcdTAwMWLOXHUwMDEwi8fsXHUwMDAyXHUwMDE0nGVNz58zY1x1MDAwMLVOpVx1MDAwMVQrXHUwMDAw8F5pmlx1MDAxOKHzwTRCp3M1f96+0te2XHUwMDA07ZN6M+NcYoVAXHUwMDExXHUwMDE4XHUwMDA3XG5cZkqQTVx1MDAwZnp64y1jQ/E/ymr292CyXHUwMDBiUmT8eMfvnjGQslJIXHUwMDAzqbPoXWBNOzFGN+JudbGKp/mOKsdL9v4hbnZcdTAwMGYyjlFEjJhpKmJHrrzVw8F7hiibWFx1MDAwNoVTyMs2u3FRXmPMRlx1MDAxNOGXXHUwMDA1Rr/K0etUR1x1MDAxZjAwuVFqcojS1VJ8XqrcLe/fkSqdXHUwMDFlx2f1pTjjXHUwMDEw1dZEvFx1MDAxNHkxguH7rmhcdTAwMDCiZCHyXGZdoyUspTNsQ8k60Dxfs1x1MDAwN9DUyD1a8W9mXG5cdTAwMWK6cGVOOnBZujjcKraO7lt4WC1cdTAwMTSyrpbEiFx1MDAxYVx1MDAxM2xARlx1MDAwMIBcdTAwMWFkolx1MDAxMnRcdTAwMDJtvZO9UfA2w1x1MDAxMHWIIH/NXHUwMDFjRFx1MDAxM9ubw2JJ81CQXHUwMDE35lx1MDAxNEb08mR1fdNcdTAwMWRuz5lyQ69vXHUwMDFmXHUwMDFkNU9d1jHKojBicHi0gVx0KeJcYkh1xFqJXHSAZuWUYEWZwygvMs9E5es26b+KikK6oKdgXGZcdTAwMTjvJo866dU5t1x1MDAxN6/XTu9cdTAwMTaal1x1MDAxN9v6uqIuTzPu6I3CyGrWhSGYXHUwMDAwrNuHXHUwMDFjPdNcdTAwMDDN0FDog2Zbm12IWvLOOHKzZkY9pu6Aslx1MDAwYnTOoZ3cim51zu5q5qp71Jy/OZ3rPOT396pLv1x1MDAwMUJccoHkM1x1MDAxMftJjYOKnvldxLiV6fdK8k0yXHUwMDBiUUmFcVx1MDAwZczMIVSlclFAQ7Lpoibfoz/ZvF85r3Yr8a3/sVZcdTAwMGY/XHUwMDE2XHUwMDFh9e2TrDt6ZpqRdV6zUlx1MDAwMutJXHUwMDFiN4RRXHUwMDE3Ke15Zlx1MDAxODySSJRZjGpcdTAwMTWQzSg7v1x1MDAxOVx1MDAwM2lcdTAwMTKCw5FRXHRcdTAwMDeLkp3cjsYnj6fbtki71/a+3tneuL5sXHUwMDFk72bcjuY8S1wiq1x1MDAxMDyixGyGIFx1MDAxYVTkwLKh5eWqvcmuYDKEOlg0OGNcYvVk0lx1MDAxMFxuXjtngp5cImH04r64fFRrgz87fFivVY3dP136xXHRXHRSnVx1MDAxY+shZFLOXHUwMDE49MYn8fGEUWAmYFx1MDAwM09ccrDiT9iorGGUL3qPxs/cXHUwMDA2aDpGhdtYayffXSq3K6Z4cLBBXHUwMDFizYWmPc3fVpfOV7NuQ0FFzDSDt6BJXHUwMDEy7lx1MDAxMj5DPsAzeFhGsdhnyczYyS5cdTAwMTn11jtcdTAwMTVmjYo6n55FonlKmIPh5Ho+qLjauT1fceXNUk3lj4u3uc1Gxlx1MDAxMcomlFx1MDAwNaFcdTAwMGXEXFyUQnK3V95cdTAwMWWMioLRvFaNVpjhXHUwMDE0XHUwMDEy1vFMRXD24vaJcNJcYlxy1V4pq6cwoeeNKlxctNY3rlx1MDAxZud3ri/umuvbx0v5jFx1MDAwMzTnfOTYVbD/9sw02ZBcdTAwMGUhXHUwMDE0I9H43mlcbtbr7GaLasVkWmlQM0dE0yNOxoBiRVx1MDAwZpNDdD9/kju4Ot5auyue3K5tl+P7++27jENcdTAwMTS0i4zYSDDIgthcZvt4XHUwMDA2MLDSZyFigoPsZtw77Sh8iVx1MDAxMSVR0MaS+1x1MDAxYYD69LIlbXje0E7u5JfW/P3F0UY9f1fePZybPyid3dazXHUwMDFlXHUwMDEyXHUwMDE1gErpZ2BcdTAwMDOEUlx1MDAxMzJcdTAwMDBQZp9cdTAwMTQpJDayRMYmk/SyhlCw8ofC7Il5m0pEKZBcdTAwMDFm31x1MDAxMyP0buW+c7y3dpFfPC0vrF+erFx1MDAxN6uF44wjNMdcdTAwMTaUfSP7XG5P1rowlMvMXHUwMDEwdZE3/FDM9FhLZdeIXHUwMDAyX3BKzVxcXHUwMDE2ntOpXt4rtKRhilxu+rB5XHUwMDA1153c6mbzZide2cjVu3S2k/VoXHUwMDEzXHUwMDA0JyX0SmnNUPU4lMtcdTAwMWN0ZIB6NXe9TYzsXCJcdTAwMTR4XHIpM3tcdTAwMTHRkJ5BXHUwMDEyjDdcdTAwMTRCmNzPn9aqUG7klmv1/GGN8qsrjXD/i8X8JElONmJcdTAwMDBcdTAwMDZyXHUwMDEyUVxuw9tKXGZRXGbgXHUwMDA1PFp2bTJcdTAwMGJRazWxovMzZ0RDqlx1MDAxMVxyyiNKjcTEXGI9PtotNsJu6eJHbe/wgKj12Jr/kXE/r63sbLJeZ2XIXHUwMDEwtENcdTAwMTF7z5dcclx1MDAwYnmW+0g6yzZUS4WZXHUwMDEw5lx1MDAxOUNosiXASFx1MDAwMb1cbsRuz07u51x1MDAwZo/Xmyu1tZW9fGttk1x1MDAwZTr7P47aWe+Ug0iRXHUwMDA3xTSUXHUwMDE5qfFhUCxcdTAwMDVcdTAwMDORl8xcdTAwMTKN4uozXHUwMDFjXHUwMDEyXHUwMDA1RGC3Z7+uXHUwMDBiya9PwnOSXHUwMDFjaf1cdTAwMTTJ9mH+uH50Vz3rnOt5v1p5PF8s399kXHUwMDFjoVx1MDAxMHzEUt6pXHUwMDEwyFx1MDAxMlx1MDAwMNohiJqI3afmW6FcdTAwMDA1ZlfOXHUwMDA3XmdOuZkjokmjMVJWXHUwMDE3eN5cdTAwMTT5yY3oztaGu83hXHUwMDBmOHi4rN+7Zd05Xy1nXHUwMDFjokYxPFhcYvP8aoU8yYP7St6GyFx1MDAxMot6pqhW9kazXHUwMDBiUbJGh2SkcjYgXHUwMDFh0oP2rFx1MDAwZVGSeCc3okvncb11s32+7GF9fu9q5ep+Md7LPELFzTNcdTAwMTNVjs1cdTAwMTCoQVx1MDAxYtqLifJKZfNqgkXMbupcYmhePqxcdTAwMWRmzoiG9JaNzLxcdTAwMDKriCmI6KLbuDutLS0+xlx1MDAxN3GzVlx1MDAwN79ysXKdcYSy9YxcdTAwMTTrdc2zz25cXHk3XGZR2Vx1MDAxOVxyXGJEPDtJnpc5jFx1MDAxYalO1cltltnAKGLqzlx1MDAxMnMvmVx1MDAxYTOFoK/eXHUwMDE11pY3fdw9OV9cXHbrXHUwMDA1v7dRTstwXHUwMDFhXHUwMDAy2yBEh5MxX95VKrSr5Skw6t6M23stXHUwMDAxJ03GSdxpOG4vtclcdTAwMTakpNJcdTAwMTHpgV2Md2C00yrU281CizEwilNcIlx1MDAxM1nrPDnwXHUwMDAxKVHH/4JTRmjU6yhFxORcdTAwMTleazkmdWZOm4/D6fOFPrBcdTAwMTLz3XAnq9sneq55tN8+qtRaoW7z/eZdXHUwMDAzKCy0Wo377y9X/vpz3OfmjzcvVeGH3aruXHUwMDE31U7zvFx1MDAxM2/Q2mSf+/y/9NUlOb+e7JesLvBcdNo4XHUwMDFjjFx1MDAwMKc1QjLx/M1cdTAwMDYqZ5VC52Jne7+Rq+J5cXl/f8WkXHUwMDA1I963vD6jl5+xnsh6wzKPwlA9IPpcYlxyYOhcdTAwMTUyJWNR73FcdTAwMDCVSmV0VaHxXHUwMDExSfKXXHUwMDE2nenBj64q9kxRkEpcdTAwMDZcdTAwMTabVnt6JVx1MDAxMFx1MDAxMbTU3NJcdTAwMTetquPS0YK+qdxsVbeDtY15u5M/+Fx1MDAxMPSzYFx0XHUwMDA2TKI4+FP5z7jmQc6LuPeT59W44upGvXje6Txs3Hk791jaNLfdrG9nXHUwMDEwRYjSpJLBbchYN0jSrYFcYp2072b+O1DSnTVcdTAwMDLkXHUwMDAwjTIztydcdTAwMWN8KkQt36vB1ohvIXRt7vDuPr46NKcnm4vL84dm3u1cdTAwMTWzjlA2XGKRtuRQ2loo0GZcdTAwMDSgXG6sVkxcdTAwMDSlnjvLXGZdci9cdTAwMDLNXHUwMDFjQ1x1MDAwZphKIShoUfgwOUT90Ur+qHvjVudcdTAwMTf2N+7McYxcdTAwMGKrv7hcdTAwMDXbJHvCOnJse1x1MDAwMtPv4IJcdTAwMWJuXGJsdIRSbOhcdTAwMDP77iz3Zlx0soPPTmDWrCgkO+BcdTAwMGXvXG5b8D55/S2Idlx1MDAxZuau4kK8tH661z09PVxu5/Gmb72H5Fx1MDAwZWdYfSrJVYrvOfPLQJLLPeTlmeU6XHUwMDE2kFxmUCvJXHUwMDBin0FyXUSGx4DSXHUwMDAwjswr0lx1MDAxMYRpI7Di8NIlXHUwMDA2kpU6zylcdTAwMGLaK0Tzgbtt40iu1t2HjXZcdTAwMWL2701cdTAwMWTWXHUwMDFmdtbDfq09XHUwMDE5yVx1MDAxZCtcdTAwMWQ/jzyLdERNiT5EP7mmynFcXGu2X19RXGLpXHKMTVxiXoVpXHUwMDEyLVx1MDAxNnKrj2v7W4fY3cH25Wlz+X47Tlx1MDAwYlx1MDAxZY5dU19n9C1cdTAwMTNcdTAwMGY5QoOkr2qya+GTySemJazJWLO5nywur1x1MDAxNIgndXRNiWzkJcvvN8yMkod1vKwpzz5Bu15tZq9dPVx1MDAwZS8pXGLa9PqGftzmyyetqVx0sG9cdTAwMDIl5NpnYj/9XGJcdTAwMDRcbkRopombP6jaSfVu3Vx1MDAwMdxvdCvhcPn29lhnXHUwMDFi+o5NtVx1MDAxNKVbhrZU21xmd5dnwi6tXHUwMDE0fUBG10/muldcbufsNj5cdTAwMDf90tvS0kfGXCK/jO6Mg6fRqZnuLOSZkSdcdTAwMWKevNlxtrtztHd80a7nW/VcdTAwMDNLues18svZhmewXHUwMDExXHUwMDFiTNTKXHUwMDA1ZYJcdTAwMWVcdTAwMDInRp5cdTAwMTcwOEmCI/dz7ZNAn3v/XG5cdTAwMTn/XGJwamM86I9cZullg4uH9MM5XHUwMDAwyTgzVUHwzdpybVWIgzt5rN/O6at7XFz+xVxyviag45YxXHUwMDE4sKdcdTAwMTiJkseS9XaE5Fx1MDAxNC7JVWVcdTAwMTHHbi3D7ZNcdTAwMDC0lb2emeuflOxcdTAwMDc0sunII1HgpijFWHY3NVx1MDAxN9YrjYWthePOXFzturyx+Iubdk9cdTAwMTbTXGJI5Fk3SuR30MdLb1x1MDAxYalcdTAwMTFinSapVlnO0eTRofPqd3Ty41x1MDAxYsun9+xcdTAwMTZS79n9TZFFvJu/xU13eLOoluY3Nu5cdTAwMGXndWpBW0a8PLmIuSVbSXa0npKHi/bS30izXHUwMDExtU66RcqRmz+Fz2K5ZEqFUXxcIjNddFxundXW8WJJxP36YVxyadfoXHUwMDAz01xyXHSvkLV2XHUwMDE4ob2oh0P4uKDb84VXJZhdpq2DPD6usXBdOsvRwsrZ1u6HSDBS4I2ihPP+3NyldPgzXHUwMDE4PNI0JLecK8Depe80fzzg/FK3splfzDyLYFx1MDAwNlx1MDAxMTltSY5HkiNA9GhrJqWsXHUwMDFjQ2d5WnR2O9z5XkqJnTlcdTAwMGLtbWrqktfM7aU75sRcdTAwMTA9idtzuNxdPDpRh4Vt6zqF5TnMtoVmmVx1MDAxOVnNXCLLgGPFZf1QhjKZKEhcdTAwMGZjJZlcdTAwMGL+55SYKdpy5ZUwXHUwMDAxhFx1MDAxMCErXlxiWnpEqVeyK0BHXGZdbaWbmzch2YbvpVaOv1x1MDAwMdFcdTAwMTeckyh7mNKV+otork6VYsZpdp9qinbgJzfdrd2TucJ6Z2N+XHUwMDAzf1xcqIUlyP7mssZIXHUwMDEykUny5L0mNdwllFx1MDAxMexcclwiMpOg5Jl9WbOhILFcdTAwMGXnZq9tXHUwMDAzr9s0jLLR0EK/pkhQ6yzUtqq1dtXNVcxyI3dZc0fl86yDlH28VFx1MDAxYrFcdTAwMTdHZ1xmQ3FcdTAwMTikIdLKMlYlXFyU7H+XNZDyTIGXVNZcdTAwMTnDqLepXHUwMDE5XHUwMDEwutf6Jlnc8maK8uJBWHs4XzgrmJ3H5nGhvFs8Szu0OyOOXHUwMDFlvY20Zi7qJf84ebhuL+RKllVSYIGDyqJcbj+HzzQtXHUwMDA2XHUwMDE4XHUwMDE5SZy1JFx0kjYk8lr7nj5cdTAwMTg5asyB9JHSNimQ+2n0Tto2fGCz5edcdTAwMGKvirHV05qNXHUwMDBm6o+ddkGdLOxuLV11XHUwMDFmXHUwMDFiXHUwMDFmJsYsa86vKmlO7zcuaWrKXHUwMDEyTUF1YblY7Fx1MDAxZW7XdpfV5W14XFw+0EtpR4VmZlx1MDAwNfhcYqw2IHRXXHUwMDA1sINU17JUQ4c6OHRcdTAwMWHDz+Xop3Nd6SYtNNcz0KVcdTAwMWPkXHUwMDE1XHUwMDFi7bU0o1RSXCLZq2hcdTAwMTnhutJtWtpXfH41nnBdzWPwXHUwMDFmXHUwMDA10U6523lccp2AqfthXHUwMDE2jSfvp+jivFvK32xf7NFK8+K+e53vXFyW2+4h21x1MDAxNSSGNZBUhmgmXHUwMDEx0qTFXHUwMDBlZv8443vZa1oh00c95qjlYtBcdTAwMDVdXHUwMDE4XHUwMDBmzlfTfzS9XHUwMDAyRVx1MDAxY4mEsdFmn/1VkbDPqlxyOVretcVcXLFcdTAwMTCftrDbXTg9PL3V+1x1MDAxZmPUwTnJ0ZsmO76PzEa9s1977JFcdTAwMTI/8Oxy4bpcdTAwMTY/XGagq7eUeIB3tVbntlx1MDAxMJ+1+Y3R88wlpr9d5lx1MDAwMfQ+kVx1MDAwNt46XHUwMDE31y5k5X2Py5XBJdmp8Y15udxpJPZBijyUXHUwMDAyf1xca600/JVcdTAwMWGt2kWtXohcdTAwMGbGXGZrrGl4utGv2Fx1MDAwNkxEzUZavKtARlx1MDAxMuYmNlx1MDAwZeMh9Vx1MDAwYlJcdTAwMDPftFxyrHgjJsug5HBoo0lcdTAwMGZpXHUwMDBipFxiia2D6Vx1MDAxNVx1MDAwN2D6Ps+7bVx1MDAwM0aKjY72Qlx1MDAxMVx1MDAxZDP2V1JcdTAwMDO1ZFNcdTAwMTkjXHQnhjTzs4RcdTAwMGV/5m1eNtNcdTAwMDdcZvknNtZkXHUwMDFmbmCaXHUwMDAyrcRdK7Q687V6qVa/4It96/G9/PSr1yZwMvLlXG7NJ84rgW6ggIHlTmJHqLeyi7fyRXIqYuIth5rx/Vx1MDAxNYLs9fOLXszY93K99PagxtdOvlxmXG4hXG5cdTAwMDY8z1x1MDAwNlx1MDAwMYY+MUxcdTAwMGVcdFwiXHUwMDAwNCxmXHSZq0uq0MiQ4kK7s9C4vq51+NbvNGr1zvAt7t3LOVnY1XJhxGTwV0peXHUwMDFitlx1MDAwME35xEFcdTAwMTfR/9+3/lwi6f3w8v9///nqq3OpXHUwMDE47l1cdTAwMWSFb/9cdTAwMDP/SP47tfWChMBcdTAwMWSOMFtWOuSmqF9cdTAwMWHvtzJpvEBH3rDMYTNtTbJT0NNcdTAwMDZcYi9cdTAwMGZcdFlIK1x1MDAxMGV9+lx1MDAxZeD7eU1cdTAwMTQoKC9dNlx1MDAxOMc6XHUwMDExIO7brmCjoK2R9sRcdTAwMDEg8ZKXJFxuOaieXHUwMDE11Fx1MDAxN5kuyUv6labrbyOgXCLvXHUwMDAzotXBs26X8uVcdTAwMTFT4iNWjjx34L0odlx1MDAxYsZcdTAwMWKuwW/xO9mPVFx1MDAxY8ljXHUwMDA0QVNajzRZpCn9ZFx1MDAwNulcdTAwMTQ71WGgjcOHXHUwMDFm165Rblx1MDAxNvPH7W3TXHKUO3pXXHUwMDAywdfJXCJcdTAwMWZcIr7dbLVccitAlviDXHRcdTAwMDRS7Fx1MDAxOFx1MDAxOSl6ZG2PaMaEVd9vPsxEskgybFx1MDAxZPlcdTAwMGY8XHUwMDFh5Fx1MDAwNUFcdTAwMWZb2j5WXHUwMDE2nd0wub9eKF0snrHMXFw92NleK1Y/TFx1MDAxNiEkj/b5Uln0NJtZU0VPo3pcdTAwMWat0JTa5Vx1MDAxNcBrfkyTWzRcdTAwMWVQWeRcdTAwMTVomNDJKdaS407SXHUwMDA1Ycg0aOZcdTAwMWSS7Vwivf9w3JlC7zZccqBs5PlXM4lmiFx1MDAxYlx1MDAxNmCjllx1MDAwMlwiy0KEiU+Qbp8+JOImf1x1MDAwN1RcdTAwMWPLOvz8LcHeXG7kXHUwMDFi9pnEYryHeeFcZiHSgVmOddDrVKhe1Vx1MDAxZipSXHUwMDEyXHSTvFx1MDAxOSXpq1x1MDAwNv92qp+jibSP2JtKfYpcdTAwMDe+UaTptVFcdTAwMDGDTskmXHUwMDA2Sk5cdTAwMDTZ31xcXHUwMDE1pUNYXHUwMDFluVH0Tslr0lWRT1VFYCE4YG00+W7EeL+VRfPlMFx1MDAxMpWvjFx1MDAxM3R7P3hYXG7IUVx1MDAxNc4rJGv4XHUwMDBiXHUwMDAxpSeGvd96XHUwMDExK3xvXHUwMDA1x57XWLKLRtJ6OWW8XHUwMDBiXHUwMDE2pUklJk6w+dt6MS4gfMX50T37ReinOSzlw+1XwjpcdTAwMDFTUtb2pEVcIklzvFx1MDAxMYtcIr3ztDNcdTAwMWGZXHUwMDE4PjWOXHUwMDE5b8B+X22UjqWnyyMwmtKMpMqj9Fx1MDAwZaPA9EeiXG6Tx1Z2qiv33fg0//DwuEA4lzdUryz+SiNcdTAwMTLebDuGkWbbgFx1MDAwZYyWznh+qI84Id95w9zH2+CtXHUwMDBi6Y03Pl1cdTAwMWX1TqtcdFx1MDAxZqmPxm7AiDv7MqXBWqJcdTAwMTK1i61GXHUwMDFjnzUqlXY5XHUwMDEz+y+vjOp9rlx1MDAxYdPPk5De4OzFpuhcdTAwMWQyvro4i5465yhcdTAwMDIkQMnRXHUwMDA2tGYwbcBcdTAwMDTV27jl22B6yPtcdTAwMDRPrSEyVlpDsFx1MDAwNe31XHUwMDBifsVTW4ycXHUwMDFjQ+2c8sriaP8xLWedOPNcdTAwMTVH8vTKjtFOVWWQ5qqHndrbTnx8n4KkXHUwMDEzN9LNwmtcdTAwMDfA3JZvT1KHvLhwXGbSXHUwMDE1RrxcdFwi64Lfm+6nXCKpd3VcdTAwMDRDXHUwMDFm5aZNelx1MDAxNFODYoJcdTAwMTTc5CZk78fVdv3i7qh2eXuSL1x1MDAxMFx1MDAxZZRgPc62n2ZcdTAwMDSxbpRjXGKIv21wg7FcbsZfXHUwMDA0ipRccrw+dXr+8lx1MDAwNFx1MDAwNmRsb9BkXHUwMDFisnG+mlx1MDAwNWFgsvCBqfQvOPraRoVshuQgx2mSl36OXHQ8fPvXt2eX+5BcdTAwMDVcdTAwMGUwMJ73eX9Krz+wcniNXHUwMDA1mGJcdTAwMDdi7FxcZ3LpgjKR8axt2KH1jl1cdTAwMWNO7MaIlKxr1oCa1d7nrV45csVcdTAwMWE54JeRbYx+Ra9cdTAwMWJcdTAwMTVZXHUwMDFlJZCXhlnJY5ZeSlx1MDAxMFTvaFb7JVx1MDAxYpk2KLTTdDpKI1x1MDAwMWmufrwr+JbcyNTBXHUwMDFi6Vx1MDAwZq/43imnXHUwMDEzju9vV29cIkmxMUH6t1wiWffOXHUwMDE0jPH9dVx1MDAwN8bEzFlCm0DBSSTIjo7JsdvQRlx1MDAwZXSWg1x1MDAwNdG635t+pEJYXHUwMDFluVx1MDAxMfR+XHUwMDEw/TDpLWQtXHUwMDBmiW/uXHUwMDE0UVx1MDAwMr2Tay3Gu3OhXHUwMDFiXHUwMDFmrlx1MDAxY9au1095rNk2YY6RJlx1MDAxOS1cdTAwMWV9YJpLQ+fkWFx1MDAxMzHzYzdpJfhtf2pcdTAwMGZ1vFx1MDAwNXP97zyWf/RORvvALZFx9ONcdTAwMTNrrcVcdTAwMDBcdTAwMDZy7zKA76JcdTAwMWbdvrvvZoF+XGaM5330I6TnfjJVZuOYrGR5a+2On+ssrl1cYkI+iFx1MDAwNVx1MDAxMlx1MDAxODaINFx1MDAxON9jK1x1MDAxYSlWrsRY41x1MDAxN4w5KLDE2kNV3r900UYuKM1SmNiDUnilWVx1MDAxMig2MyCWm6S1XHUwMDA1hNFcdTAwMDRQXHUwMDA34lk/Mnd83Nqz7FPDe9behORjvCf4NriVSSGgxEDZXHUwMDE3XHUwMDE5VoB21NeHSI5cdTAwMDBDXHUwMDA0lDzEN7ZcbtLGNL5HSGJM0ndAK9Deg3RQNyPDXHUwMDE53Wz9nZhGKl7lkVx1MDAxYoXqXHUwMDA3MVxyXHUwMDFjc+KZQUd2mkT1Jbuy0L09OvUnav/qqlalo/gw20xDo5aTRcBcdTAwMDVpyy9lVkNMQzNcclx1MDAwN81UXFzOQlCfp5X0a1x1MDAxOepuJK2TSbXnJfmB5+uOY1x1MDAxYZ9YSFxiTkqs35fa8dNM49t//1/9abNhfKaVXHUwMDFk/JivJFx1MDAxZq9ccvF9fFx1MDAwNCmVj0ibNctcdTAwMTR7cj4yXHUwMDFlXHUwMDExWVxc4dJNSTn+jqC0XHUwMDFmboondFx1MDAwNMiSXCKjpXz3p5JcdTAwMTbGrm+j5UBcdTAwMDGRiprFvE+e+fKy3IOPWGqCNEsnSc1cdTAwMWStI0ZpP0lIX5J7xVx1MDAxYd6bz6xHXHUwMDE57y6G6Ij1VsuBjspcdTAwMWG+gf1K6lx1MDAxN/fvXCIp/tCyU8BcdTAwMDTUvZXWnZp7NbZQfnBQksZneGBOyzlvYTRCQ1x1MDAxMSrpt2htUMF781szlHRcYssjN4LeNIbyx/Nv+F5oNvc7jLmX2WA010rPlr3/Nb/f1cr3869vPcru41x1MDAxZs83VKxPWb7sf/7646//XHUwMDA3cfF+/CJ9 - virtual_size.heightvirtual_size.widthself.scroll_offsetscroll_yscroll_xscroll_x +self.size.width + virtual_size.heightvirtual_size.widthself.scroll_offsety = scroll_yx = scroll_xx = scroll_x +self.size.width From de2a4fd78d3fb14ee01109b0a10db3ee252053a8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 4 Feb 2023 15:53:00 +0100 Subject: [PATCH 08/17] test fixes --- tests/test_styles_cache.py | 49 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/tests/test_styles_cache.py b/tests/test_styles_cache.py index fdeb72dd6..a436c846b 100644 --- a/tests/test_styles_cache.py +++ b/tests/test_styles_cache.py @@ -10,7 +10,7 @@ from textual.geometry import Region, Size from textual.strip import Strip -def _extract_content(lines: list[list[Segment]]): +def _extract_content(lines: list[Strip]) -> list[str]: """Extract the text content from lines.""" content = ["".join(segment.text for segment in line) for line in lines] return content @@ -28,9 +28,9 @@ def test_set_dirty(): def test_no_styles(): """Test that empty style returns the content un-altered""" content = [ - [Segment("foo")], - [Segment("bar")], - [Segment("baz")], + Strip([Segment("foo")]), + Strip([Segment("bar")]), + Strip([Segment("baz")]), ] styles = Styles() cache = StylesCache() @@ -54,9 +54,9 @@ def test_no_styles(): def test_border(): content = [ - [Segment("foo")], - [Segment("bar")], - [Segment("baz")], + Strip([Segment("foo")]), + Strip([Segment("bar")]), + Strip([Segment("baz")]), ] styles = Styles() styles.border = ("heavy", "white") @@ -85,9 +85,9 @@ def test_border(): def test_padding(): content = [ - [Segment("foo")], - [Segment("bar")], - [Segment("baz")], + Strip([Segment("foo")]), + Strip([Segment("bar")]), + Strip([Segment("baz")]), ] styles = Styles() styles.padding = 1 @@ -116,9 +116,9 @@ def test_padding(): def test_padding_border(): content = [ - [Segment("foo")], - [Segment("bar")], - [Segment("baz")], + Strip([Segment("foo")]), + Strip([Segment("bar")]), + Strip([Segment("baz")]), ] styles = Styles() styles.padding = 1 @@ -150,9 +150,9 @@ def test_padding_border(): def test_outline(): content = [ - [Segment("foo")], - [Segment("bar")], - [Segment("baz")], + Strip([Segment("foo")]), + Strip([Segment("bar")]), + Strip([Segment("baz")]), ] styles = Styles() styles.outline = ("heavy", "white") @@ -177,9 +177,9 @@ def test_outline(): def test_crop(): content = [ - [Segment("foo")], - [Segment("bar")], - [Segment("baz")], + Strip([Segment("foo")]), + Strip([Segment("bar")]), + Strip([Segment("baz")]), ] styles = Styles() styles.padding = 1 @@ -203,17 +203,17 @@ def test_crop(): assert text_content == expected_text -def test_dirty_cache(): +def test_dirty_cache() -> None: """Check that we only render content once or if it has been marked as dirty.""" content = [ - [Segment("foo")], - [Segment("bar")], - [Segment("baz")], + Strip([Segment("foo")]), + Strip([Segment("bar")]), + Strip([Segment("baz")]), ] rendered_lines: list[int] = [] - def get_content_line(y: int) -> list[Segment]: + def get_content_line(y: int) -> Strip: rendered_lines.append(y) return content[y] @@ -232,6 +232,7 @@ def test_dirty_cache(): del rendered_lines[:] text_content = _extract_content(lines) + print(text_content) expected_text = [ "┏━━━━━┓", "┃ ┃", From 59def1a591b56246fc83f366b7dc325e031d8a79 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 4 Feb 2023 16:10:33 +0100 Subject: [PATCH 09/17] test fix --- src/textual/_styles_cache.py | 2 +- tests/test_styles_cache.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/textual/_styles_cache.py b/src/textual/_styles_cache.py index dea21a3fb..4a8a74208 100644 --- a/src/textual/_styles_cache.py +++ b/src/textual/_styles_cache.py @@ -212,7 +212,7 @@ class StylesCache: padding: Spacing, base_background: Color, background: Color, - render_content_line: RenderLineCallback, + render_content_line: Callable[[int], Strip], ) -> Strip: """Render a styled line. diff --git a/tests/test_styles_cache.py b/tests/test_styles_cache.py index a436c846b..01045e322 100644 --- a/tests/test_styles_cache.py +++ b/tests/test_styles_cache.py @@ -227,12 +227,13 @@ def test_dirty_cache() -> None: Color.parse("blue"), Color.parse("green"), get_content_line, + Size(3, 3), ) assert rendered_lines == [0, 1, 2] del rendered_lines[:] text_content = _extract_content(lines) - print(text_content) + expected_text = [ "┏━━━━━┓", "┃ ┃", From dc1fce3da78df77d45e8c19b4d86458104b8ccf6 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 4 Feb 2023 16:13:42 +0100 Subject: [PATCH 10/17] svg update --- docs/images/scroll_view.excalidraw.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/images/scroll_view.excalidraw.svg b/docs/images/scroll_view.excalidraw.svg index 4dfc06120..3d78e166c 100644 --- a/docs/images/scroll_view.excalidraw.svg +++ b/docs/images/scroll_view.excalidraw.svg @@ -1,6 +1,6 @@ - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1da1NcIsuy/b5/xcTcj3fTp7Kyslx1MDAxZSfixFxy309866j3njCQh6AtIKCiJ/Z/v5nollx1MDAwNmxcdTAwMDRHnVx1MDAxZZiImZFcdTAwMDYsulZlrpWVmfWfP759+955aJa///Pb93K3WIhrpVbh/vuf8vxdudWuNep8Sfd+bjduW8XeK6udTrP9z3/847rQuip3mnGhWI7uau3bQtzu3JZqjajYuP5HrVO+bv+P/L1VuC7/q9m4LnVaUf+X5MqlWqfRevpd5bh8Xa532vzp/8s/f/v2n97fidG1ysVOoX5cdTAwMTGXe2/oXepcdTAwMGbQXHUwMDAyXGY/u9Wo91x1MDAwNkvgwVtcInx5Qa29yL+uUy7x1VxuXHUwMDBmudy/XCJPfd/frEC9fNndLc7nWlx1MDAwN+FylZpnK/3fWqnF8X7nIX66XHUwMDEzhWL1tpVcdTAwMThTu9NqXFyVf9RKnSpfh6HnX97XbvBN6L+r1bi9qNbL7fbAe1x1MDAxYc1CsdZ54OdQvTz5dFx1MDAwZv75rf9Ml3/KeVx1MDAxZJFcdTAwMDZcYoBeXHUwMDFi1C9cdTAwMTfl3cb7yJBzqFBrXHUwMDE1XGJwaFhcdTAwMGKNmCeCh/VfqvfoXHUwMDBm7LxQvLrg0dVL/ddo7c/Ltv+a++cva22kjHNktCFvVTAvr6iWa1x1MDAxN9VcdTAwMGW/xOnII1llrdLKe99cdTAwMWZnu9ybXHUwMDBmXHJoXGZfXHUwMDBm7uWK/PrmWqlcdTAwMDeNfyfvWL30fMfqt3HcXHUwMDFmsVxcWErAqf+e22ap8DTtYHmYxlrCYMLL9bhWv1x1MDAxYf64uFG86iOl9+xff75cdTAwMDOhzvs0hII1oFxirHJcdTAwMTNDdPGwcnXZndu6yeOP3f3Kwd3p4eJcXMYhSjZcdTAwMDIyzqLRXHUwMDBl+XZcdTAwMDQ7XG5SxEBcYoxTdGAzXHUwMDBiUlx1MDAxZb5cdEFZmjGMWqfTMFx1MDAxYZx2wTlcclx1MDAxM0O0fLC6znfv/Lxk9nbya35jY+fKZFx1MDAxY6I5wEhB8Fx1MDAxNMCx0/BcdTAwMWVcdTAwMDYxSlx1MDAxNiNjveJLoL1cdTAwMTlcdTAwMWVXdlx1MDAxMFxuhOj5XHUwMDBmzFx1MDAxYUJtSEMoKMO+g1x1MDAxZNzkjv54+3xr44T8eWNn97pwfLfK/Kb9ayFcbupNM2pcIkfMZ7S3XHUwMDEw2HO4XHUwMDAxhFpcZlx1MDAxMZOAXHUwMDEw2EIpMqQzXHUwMDBiUVxyzlx1MDAxOVx1MDAxZerMQdSnU1HNTs8nuc1bXGK9zp1cdTAwMTZwuTh3uH9avSovVneWVmLIulx1MDAxMfVcdTAwMTAxXHUwMDBiNVx1MDAxZa32vCpcdTAwMTNmsodQo1witqCGXGZcdTAwMThero4yi1BwPF3EPmDmuCilc1HPRoPvXHUwMDE3hIkxXHUwMDFhXFzBUty4vLwtXHUwMDFlwll8W2psu62MY1x1MDAxNDRFwTLpdsBcdTAwMTBcYlx0QfTk5ompKPNxr9EoTMx/5iDaey8wX/kqiJL6XHUwMDFhiDpMhSgp65yiKbho63G9OueWtlxiVlexnFt8XFzeqVx1MDAxZWXd0fPcsqdnXHUwMDBl5zTxv+RcdTAwMDdFPSM30p61vLXaXHUwMDA0rbNMRpVcdTAwMDfvlJo9kNpUkFx1MDAwNlx1MDAwYoaUm9yMzuF56Fxcrz36RnV1f+nwXHUwMDA3XHUwMDE0jupZl/S5XHUwMDEwKYW8XHUwMDFjeTGyx1x1MDAwZs5cdTAwMGZBlFwi1DxcdTAwMWLOXHUwMDEwi8fsXHUwMDAyXHUwMDE0nGVNz58zY1x1MDAwMLVOpVx1MDAwMVQrXHUwMDAw8F5pmlx1MDAxOKHzwTRCp3M1f96+0te2XHUwMDA07ZN6M+NcYoVAXHUwMDExXHUwMDE4XHUwMDA3XG5cZkqQTVx1MDAwZnp64y1jQ/E/ymr292CyXHUwMDBiUmT8eMfvnjGQslJIXHUwMDAzqbPoXWBNOzFGN+JudbGKp/mOKsdL9v4hbnZcdTAwMGYyjlFEjJhpKmJHrrzVw8F7hiibWFx1MDAwNoVTyMs2u3FRXmPMRlx1MDAxNOGXXHUwMDA1Rr/K0etUR1x1MDAxZjAwuVFqcojS1VJ8XqrcLe/fkSqdXHUwMDFlx2f1pTjjXHUwMDEw1dZEvFx1MDAxNHkxguH7rmhcdTAwMDCiZCHyXGZdoyUspTNsQ8k60Dxfs1x1MDAwN9DUyD1a8W9mXG5cdTAwMWK6cGVOOnBZujjcKraO7lt4WC1cdTAwMTSyrpbEiFx1MDAxYVx1MDAxM2xARlx1MDAwMIBcdTAwMWFkolx1MDAxMnRcdTAwMDJtvZO9UfA2w1x1MDAxMHWIIH/NXHUwMDFjRFx1MDAxM9ubw2JJ81CQXHUwMDE35lx1MDAxNEb08mR1fdNcdTAwMWRuz5lyQ69vXHUwMDFmXHUwMDFkNU9d1jHKojBicHi0gVx0KeJcYkh1xFqJXHSAZuWUYEWZwygvMs9E5es26b+KikK6oKdgXGZcdTAwMTjvJo866dU5t1x1MDAxN6/XTu9cdTAwMTaal1x1MDAxN9v6uqIuTzPu6I3CyGrWhSGYXHUwMDAwrNuHXHUwMDFjPdNcdTAwMDDN0FDog2Zbm12IWvLOOHKzZkY9pu6Aslx1MDAwYnTOoZ3cim51zu5q5qp71Jy/OZ3rPOT396pLv1x1MDAwMUJccoHkM1x1MDAxMftJjYOKnvldxLiV6fdK8k0yXHUwMDBiUUmFcVx1MDAwZczMIVSlclFAQ7Lpoibfoz/ZvF85r3Yr8a3/sVZcdTAwMGY/XHUwMDE2XHUwMDFh9e2TrDt6ZpqRdV6zUlx1MDAwMutJXHUwMDFiN4RRXHUwMDE3Ke15Zlx1MDAxODySSJRZjGpcdTAwMTWQzSg7v1x1MDAxOVx1MDAwM2lcdTAwMTKCw5FRXHRcdTAwMDeLkp3cjsYnj6fbtki71/a+3tneuL5sXHUwMDFk72bcjuY8S1wiq1x1MDAxMDyixGyGIFx1MDAxYVTkwLKh5eWqvcmuYDKEOlg0OGNcYvVk0lx1MDAxMFxuXjtngp5cImH04r64fFRrgz87fFivVY3dP136xXHRXHRSnVx1MDAxY+shZFLOXHUwMDE49MYn8fGEUWAmYFx1MDAwM09ccrDiT9iorGGUL3qPxs/cXHUwMDA2aDpGhdtYayffXSq3K6Z4cLBBXHUwMDFizYWmPc3fVpfOV7NuQ0FFzDSDt6BJXHUwMDEy7lx1MDAxMj5DPsAzeFhGsdhnyczYyS5cdTAwMTn11jtcdTAwMTVmjYo6n55FonlKmIPh5Ho+qLjauT1fceXNUk3lj4u3uc1Gxlx1MDAxMcomlFx1MDAwNaFcdTAwMGXEXFyUQnK3V95cdTAwMWWMioLRvFaNVpjhXHUwMDE0XHUwMDEy1vFMRXD24vaJcNJcYlxy1V4pq6cwoeeNKlxctNY3rlx1MDAxZud3ri/umuvbx0v5jFx1MDAwMzTnfOTYVbD/9sw02ZBcdTAwMGUhXHUwMDE0I9H43mlcbtbr7GaLasVkWmlQM0dE0yNOxoBiRVx1MDAwZpNDdD9/kju4Ot5auyue3K5tl+P7++27jENcdTAwMTS0i4zYSDDIgthcZvt4XHUwMDA2MLDSZyFigoPsZtw77Sh8iVx1MDAxMSVR0MaS+1x1MDAxYYD69LIlbXje0E7u5JfW/P3F0UY9f1fePZybPyid3dazXHUwMDFlXHUwMDEyXHUwMDE1gErpZ2BcdTAwMDOEUlx1MDAxMzJcdTAwMDBQZp9cdTAwMTQpJDayRMYmk/SyhlCw8ofC7Il5m0pEKZBcdTAwMDFm31x1MDAxMyP0buW+c7y3dpFfPC0vrF+erFx1MDAxN6uF44wjNMdcdTAwMTaUfSP7XG5P1rowlMvMXHUwMDEwdZE3/FDM9FhLZdeIXHUwMDAyX3BKzVxcXHUwMDE2ntOpXt4rtKRhilxu+rB5XHUwMDA1153c6mbzZide2cjVu3S2k/VoXHUwMDEzXHUwMDA0JyX0SmnNUPU4lMtcdTAwMWN0ZIB6NXe9TYzsXCJcdTAwMTR4XHIpM3tcdTAwMTHRkJ5BXHUwMDEyjDdcdTAwMTRCmNzPn9aqUG7klmv1/GGN8qsrjXD/i8X8JElONmJcdTAwMDBcdTAwMDZyXHUwMDEyUVxuw9tKXGZRXGbgXHUwMDA1PFp2bTJcdTAwMGJRazWxovMzZ0RDqlx1MDAxMVxyyiNKjcTEXGI9PtotNsJu6eJHbe/wgKj12Jr/kXE/r63sbLJeZ2XIXHUwMDEwtENcdTAwMTF7z5dcclx1MDAwYnmW+0g6yzZUS4WZXHUwMDEw5lx1MDAxOUNosiXASFx1MDAwMb1cbsRuz07u51x1MDAwZo/Xmyu1tZW9fGttk1x1MDAwZTr7P47aWe+Ug0iRXHUwMDA3xTSUXHUwMDE5qfFhUCxcdTAwMDVcdTAwMDORl8xcdTAwMTKN4uozXHUwMDFjXHUwMDEyXHUwMDA1RGC3Z7+uXHUwMDBiya9PwnOSXHUwMDFjaf1cdTAwMTTJ9mH+uH50Vz3rnOt5v1p5PF8s399kXHUwMDFjoVx1MDAxMHzEUt6pXHUwMDEwyFx1MDAxMlx1MDAwMNohiJqI3afmW6FcdTAwMDA1ZlfOXHUwMDA3XmdOuZkjokmjMVJWXHUwMDE3eN5cdTAwMTT5yY3oztaGu83hXHUwMDBmOHi4rN+7Zd05Xy1nXHUwMDFjokYxPFhcYvP8aoU8yYP7St6GyFx1MDAxMot6pqhW9kazXHUwMDBiUbJGh2SkcjYgXHUwMDFh0oP2rFx1MDAwZVGSeCc3okvncb11s32+7GF9fu9q5ep+Md7LPELFzTNcdTAwMTNVjs1cdTAwMTCoQVx1MDAxYtqLifJKZfNqgkXMbupcYmhePqxcdTAwMWRmzoiG9JaNzLxcdTAwMDKriCmI6KLbuDutLS0+xlx1MDAxN3GzVlx1MDAwN79ysXKdcYSy9YxcdTAwMTTrdc2zz25cXHk3XGZR2Vx1MDAxOVxyXGJEPDtJnpc5jFx1MDAxYalO1cltltnAKGLqzlx1MDAxMnMvmVx1MDAxYTOFoK/eXHUwMDE11pY3fdw9OV9cXHbrXHUwMDA1v7dRTstwXHUwMDFhXHUwMDAy2yBEh5MxX95VKrSr5Skw6t6M23stXHUwMDAxJ03GSdxpOG4vtclcdTAwMTakpNJcdTAwMTHpgV2Md2C00yrU281CizEwilNcIlx1MDAxM1nrPDnwXHUwMDAxKVHH/4JTRmjU6yhFxORcdTAwMTleazkmdWZOm4/D6fOFPrBcdTAwMTLz3XAnq9sneq55tN8+qtRaoW7z/eZdXHUwMDAzKCy0Wo377y9X/vpz3OfmjzcvVeGH3aruXHUwMDE31U7zvFx1MDAxM2/Q2mSf+/y/9NUlOb+e7JesLvBcdNo4XHUwMDFjjFx1MDAwMKc1QjLx/M1cdTAwMDYqZ5VC52Jne7+Rq+J5cXl/f8WkXHUwMDA1I963vD6jl5+xnsh6wzKPwlA9IPpcYlxyYOhcdTAwMTUyJWNR73FcdTAwMDCVSmV0VaHxXHUwMDExSfKXXHUwMDE2nenBj64q9kxRkEpcdTAwMDZcdTAwMTabVnt6JVx1MDAxMFx1MDAxMbTU3NJcdTAwMTetquPS0YK+qdxsVbeDtY15u5M/+Fx1MDAxMPSzYFx0XHUwMDA2TKI4+FP5z7jmQc6LuPeT59W44upGvXje6Txs3Hk791jaNLfdrG9nXHUwMDEwRYjSpJLBbchYN0jSrYFcYp2072b+O1DSnTVcdTAwMDLkXHUwMDAwjTIztydcdTAwMWN8KkQt36vB1ohvIXRt7vDuPr46NKcnm4vL84dm3u1cdTAwMTWzjlA2XGKRtuRQ2loo0GZcdTAwMDSgXG6sVkxcdTAwMDSlnjvLXGZdci9cdTAwMDLNXHUwMDFjQ1x1MDAwZphKIShoUfgwOUT90Ur+qHvjVudcdTAwMTf2N+7McYxcdTAwMGKrv7hcdTAwMDXbJHvCOnJse1x1MDAwMtPv4IJcdTAwMWJuXGJsdIRSbOhcdTAwMDP77iz3Zlx0soPPTmDWrCgkO+BcdTAwMGXvXG5b8D55/S2Idlx1MDAxZuau4kK8tH661z09PVxu5/Gmb72H5Fx1MDAwZWdYfSrJVYrvOfPLQJLLPeTlmeU6XHUwMDE2kFxmUCvJXHUwMDBin0FyXUSGx4DSXHUwMDAwjswr0lx1MDAxMYRpI7Di8NIlXHUwMDA2kpU6zylcdTAwMGLaK0Tzgbtt40iu1t2HjXZcdTAwMWL2701cdTAwMWTWXHUwMDFmdtbDfq09XHUwMDE5yVx1MDAxZCtcdTAwMWQ/jzyLdERNiT5EP7mmynFcXGu2X19RXGLpXHKMTVxiXoVpXHUwMDEyLVx1MDAxNnKrj2v7W4fY3cH25Wlz+X47Tlx1MDAwYlx1MDAxZY5dU19n9C1cdTAwMTNcdTAwMGY5QoOkr2qya+GTySemJazJWLO5nywur1x1MDAxNIgndXRNiWzkJcvvN8yMkod1vKwpzz5Bu15tZq9dPVx1MDAwZS8pXGLa9PqGftzmyyetqVx0sG9cdTAwMDIl5NpnYj/9XGJcdTAwMDRcbkRopombP6jaSfVu3Vx1MDAwMdxvdCvhcPn29lhnXHUwMDFi+o5NtVx1MDAxNKVbhrZU21xmd5dnwi6tXHUwMDE0fUBG10/muldcbufsNj5cdTAwMDf90tvS0kfGXCK/jO6Mg6fRqZnuLOSZkSdcdTAwMWKevNlxtrtztHd80a7nW/VcdTAwMDNLues18svZhmewXHUwMDExXHUwMDFiTNTKXHUwMDA1ZYJcdTAwMWVcdTAwMDInRp5cdTAwMTcwOEmCI/dz7ZNAn3v/XG5cdTAwMTn/XGJwamM86I9cZullg4uH9MM5XHUwMDAwyTgzVUHwzdpybVWIgzt5rN/O6at7XFz+xVxyviag45YxXHUwMDE4sKdcdTAwMTiJkseS9XaE5Fx1MDAxNC7JVWVcdTAwMTHHbi3D7ZNcdTAwMDC0lb2emeuflOxcdTAwMDc0sunII1HgpijFWHY3NVx1MDAxN9YrjYWthePOXFzturyx+Iubdk9cdTAwMTbTXGJI5Fk3SuR30MdLb1x1MDAxYalcdTAwMTFinSapVlnO0eTRofPqd3Ty41x1MDAxYsun9+xcdTAwMTZS79n9TZFFvJu/xU13eLOoluY3Nu5cdTAwMGXndWpBW0a8PLmIuSVbSXa0npKHi/bS30izXHUwMDExtU66RcqRmz+Fz2K5ZEqFUXxcIjNddFxundXW8WJJxP36YVxyadfoXHUwMDAz01xyXHSvkLV2XHUwMDE4ob2oh0P4uKDb84VXJZhdpq2DPD6usXBdOsvRwsrZ1u6HSDBS4I2ihPP+3NyldPgzXHUwMDE4PNI0JLecK8Depe80fzzg/FK3splfzDyLYFx1MDAwNlx1MDAxMTltSY5HkiNA9GhrJqWsXHUwMDFjQ2d5WnR2O9z5XkqJnTlcdTAwMGLtbWrqktfM7aU75sRcdTAwMTA9idtzuNxdPDpRh4Vt6zqF5TnMtoVmmVx1MDAxOVnNXCLLgGPFZf1QhjKZKEhcdTAwMGZjJZlcdTAwMGL+55SYKdpy5ZUwXHUwMDAxhFx1MDAxMCErXlxiWnpEqVeyK0BHXGZdbaWbmzch2YbvpVaOv1x1MDAwMdFcdTAwMTeckyh7mNKV+otork6VYsZpdp9qinbgJzfdrd2TucJ6Z2N+XHUwMDAzf1xcqIUlyP7mssZIXHUwMDEykUny5L0mNdwllFx1MDAxMexcclwiMpOg5Jl9WbOhILFcdTAwMGXnZq9tXHUwMDAzr9s0jLLR0EK/pkhQ6yzUtqq1dtXNVcxyI3dZc0fl86yDlH28VFx1MDAxYrFcdTAwMTdHZ1xmQ3FcdTAwMTikIdLKMlYlXFyU7H+XNZDyTIGXVNZcdTAwMTnDqLepXHUwMDE5XHUwMDEwutf6Jlnc8maK8uJBWHs4XzgrmJ3H5nGhvFs8Szu0OyOOXHUwMDFlvY20Zi7qJf84ebhuL+RKllVSYIGDyqJcbj+HzzQtXHUwMDA2XHUwMDE4XHUwMDE5SZy1JFx0kjYk8lr7nj5cdTAwMTg5asyB9JHSNimQ+2n0Tto2fGCz5edcdTAwMGKvirHV05qNXHUwMDBm6o+ddkGdLOxuLV11XHUwMDFmXHUwMDFiXHUwMDFmJsYsa86vKmlO7zcuaWrKXHUwMDEyTUF1YblY7Fx1MDAxZW7XdpfV5W14XFw+0EtpR4VmZlx1MDAwNfhcYqw2IHRXXHUwMDA1sINU17JUQ4c6OHRcdTAwMWHDz+Xop3Nd6SYtNNcz0KVcdTAwMWPkXHUwMDE1XHUwMDFi7bU0o1RSXCLZq2hcdTAwMTnhutJtWtpXfH41nnBdzWPwXHUwMDFmXHUwMDA10U6523lccp2AqfthXHUwMDE2jSfvp+jivFvK32xf7NFK8+K+e53vXFyW2+4h21x1MDAxNSSGNZBUhmgmXHUwMDEx0qTFXHUwMDBlZv8443vZa1oh00c95qjlYtBcdTAwMDVdXHUwMDE4XHUwMDBmzlfTfzS9XHUwMDAyRVx1MDAxY4mEsdFmn/1VkbDPqlxyOVretcVcXLFcdTAwMTCftrDbXTg9PL3V+1x1MDAxZmPUwTnJ0ZsmO76PzEa9s1977JFcdTAwMTI/8Oxy4bpcdTAwMTY/XGagq7eUeIB3tVbntlx1MDAxMJ+1+Y3R88wlpr9d5lx1MDAwMfQ+kVx1MDAwNt46XHUwMDE31y5k5X2Py5XBJdmp8Y15udxpJPZBijyUXHUwMDAyf1xca600/JVcdTAwMWGt2kWtXohcdTAwMGbGXGZrrGl4utGv2Fx1MDAwNkxEzUZavKtARlx1MDAxMuYmNlx1MDAwZeMh9Vx1MDAwYlJcdTAwMDPftFxyrHgjJsug5HBoo0lcdTAwMGZpXHUwMDBipFxiia2D6Vx1MDAxNVx1MDAwN2D6Ps+7bVx1MDAwM0aKjY72Qlx1MDAxMVx1MDAxZDP2V1JcdTAwMDO1ZFNcdTAwMTkjXHQnhjTzs4RcdTAwMGV/5m1eNtNcdTAwMDdcZvknNtZkXHUwMDFmbmCaXHUwMDAyrcRdK7Q687V6qVa/4It96/G9/PSr1yZwMvLlXG7NJ84rgW6ggIHlTmJHqLeyi7fyRXIqYuIth5rx/Vx1MDAxNYLs9fOLXszY93K99PagxtdOvlxmXG4hXG5cdTAwMDY8z1x1MDAwNlx1MDAwMYY+MUxcdTAwMGVcdFwiXHUwMDAwNCxmXHSZq0uq0MiQ4kK7s9C4vq51+NbvNGr1zvAt7t3LOVnY1XJhxGTwV0peXHUwMDFitlx1MDAwME35xEFcdTAwMTfR/9+3/lwi6f3w8v9///nqq3OpXHUwMDE47l1cdTAwMWSFb/9cdTAwMDP/SP47tfWChMBcdTAwMWSOMFtWOuSmqF9cdTAwMWHvtzJpvEBH3rDMYTNtTbJT0NNcdTAwMDZcYi9cdTAwMGZcdFlIK1x1MDAxMGV9+lx1MDAxZeD7eU1cdTAwMTQoKC9dNlx1MDAxOMc6XHUwMDExIO7brmCjoK2R9sRcdTAwMDEg8ZKXJFxuOaieXHUwMDE11Fx1MDAxN5kuyUv6labrbyOgXCLvXHUwMDAzotXBs26X8uVcdTAwMTFT4iNWjjx34L0odlx1MDAxYsZcdTAwMWKuwW/xO9mPVFx1MDAxY8ljXHUwMDA0QVNajzRZpCn9ZFx1MDAwNulcdTAwMTQ71WGgjcOHXHUwMDFm165Rblx1MDAxNvPH7W3TXHKUO3pXXHUwMDAywdfJXCJcdTAwMWZcIr7dbLVccitAlviDXHRcdTAwMDRS7Fx1MDAxOFx1MDAxOSl6ZG2PaMaEVd9vPsxEskgybFx1MDAxZPlcdTAwMGY8XHUwMDFh5Fx1MDAwNUFcdTAwMWZb2j5WXHUwMDE2nd0wub9eKF0snrHMXFw92NleK1Y/TFx1MDAxNiEkj/b5Uln0NJtZU0VPo3pcdTAwMWat0JTa5Vx1MDAxNcBrfkyTWzRcdTAwMWVQWeRcdTAwMTVomNDJKdaS407SXHUwMDA1Ycg0aOZcdTAwMWSS7Vwivf9w3JlC7zZccqBs5PlXM4lmiFx1MDAxYlx1MDAxNmCjllx1MDAwMlwiy0KEiU+Qbp8+JOImf1x1MDAwN1RcdTAwMWPLOvz8LcHeXG7kXHUwMDFi9pnEYryHeeFcZiHSgVmOddDrVKhe1Vx1MDAxZipSXHUwMDEyXHSTvFx1MDAxOSXpq1x1MDAwNv92qp+jibSP2JtKfYpcdTAwMDe+UaTptVFcdTAwMDGDTskmXHUwMDA2Sk5cdTAwMDTZ31xcXHUwMDE1pUNYXHUwMDFluVH0Tslr0lWRT1VFYCE4YG00+W7EeL+VRfPlMFx1MDAxMpWvjFx1MDAxM3R7P3hYXG7IUVx1MDAxNc4rJGv4XHUwMDBiXHUwMDAxpSeGvd96XHUwMDExK3xvXHUwMDA1x57XWLKLRtJ6OWW8XHUwMDBiXHUwMDE2pUklJk6w+dt6MS4gfMX50T37ReinOSzlw+1XwjpcdTAwMDFTUtb2pEVcIklzvFx1MDAxMYtcIr3ztDNcdTAwMWGZXHUwMDE4PjWOXHUwMDE5b8B+X22UjqWnyyMwmtKMpMqj9Fx1MDAwZaPA9EeiXG6Tx1Z2qiv33fg0//DwuEA4lzdUryz+SiNcdTAwMTLebDuGkWbbgFx1MDAwZYyWznh+qI84Id95w9zH2+CtXHUwMDBi6Y03Pl1cdTAwMWX1TqtcdFx1MDAxZqmPxm7AiDv7MqXBWqJcdTAwMTK1i61GXHUwMDFjnzUqlXY5XHUwMDEz+y+vjOp9rlx1MDAxYdPPk5De4OzFpuhcdTAwMWQyvro4i5465yhcdTAwMDIkQMnRXHUwMDA2tGYwbcBcdTAwMDTV27jl22B6yPtcdTAwMDRPrSEyVlpDsFx1MDAwNe31XHUwMDBifsVTW4ycXHUwMDFjQ+2c8sriaP8xLWedOPNcdTAwMTVH8vTKjtFOVWWQ5qqHndrbTnx8n4KkXHUwMDEzN9LNwmtcdTAwMDfA3JZvT1KHvLhwXGbSXHUwMDE1RrxcdFwi64Lfm+6nXCKpd3VcdTAwMDRDXHUwMDFm5aZNelx1MDAxNFODYoJcdTAwMTTc5CZk78fVdv3i7qh2eXuSL1x1MDAxMFx1MDAxZZRgPc62n2ZcdTAwMDSxbpRjXGKIv21wg7FcbsZfXHUwMDA0ipRccrw+dXr+8lx1MDAwNFx1MDAwNmRsb9BkXHUwMDFisnG+mlx1MDAwNWFgsvCBqfQvOPraRoVshuQgx2mSl36OXHQ8fPvXt2eX+5BcdTAwMDVcdTAwMGUwMJ73eX9Krz+wcniNXHUwMDA1mGJcdTAwMDdi7FxcZ3LpgjKR8axt2KH1jl1cdTAwMWNO7MaIlKxr1oCa1d7nrV45csVcdTAwMWE54JeRbYx+Ra9cdTAwMWJcdTAwMTVZXHUwMDFlJZCXhlnJY5ZeSlx1MDAxMFTvaFb7JVx1MDAxYpk2KLTTdDpKI1x1MDAwMWmufrwr+JbcyNTBXHUwMDFi6Vx1MDAwZq/43imnXHUwMDEzju9vV29cIkmxMUH6t1wiWffOXHUwMDE0jPH9dVx1MDAwN8bEzFlCm0DBSSTIjo7JsdvQRlx1MDAwZXSWg1x1MDAwNdG635t+pEJYXHUwMDFluVx1MDAxMfR+XHUwMDEw/TDpLWQtXHUwMDBmiW/uXHUwMDE0UVx1MDAwMr2Tay3Gu3OhXHUwMDFiXHUwMDFmrlx1MDAxY9au1095rNk2YY6RJlx1MDAxOS1cdTAwMWV9YJpLQ+fkWFx1MDAxMzHzYzdpJfhtf2pcdTAwMGZ1vFx1MDAwNXP97zyWf/RORvvALZFx9ONcdTAwMTNrrcVcdTAwMDBcdTAwMDZy7zKA76JcdTAwMWbdvrvvZoF+XGaM5330I6TnfjJVZuOYrGR5a+2On+ssrl1cYkI+iFx1MDAwNVx1MDAxMlx1MDAxODaINFx1MDAxON9jK1x1MDAxYSlWrsRY41x1MDAxN4w5KLDE2kNV3r900UYuKM1SmNiDUnilWVx1MDAxMig2MyCWm6S1XHUwMDA1hNFcdTAwMDRQXHUwMDA34lk/Mnd83Nqz7FPDe9behORjvCf4NriVSSGgxEDZXHUwMDE3XHUwMDE5VoB21NeHSI5cdTAwMDBDXHUwMDA0lDzEN7ZcbtLGNL5HSGJM0ndAK9Deg3RQNyPDXHUwMDE53Wz9nZhGKl7lkVx1MDAxYoXqXHUwMDA3MVxyXHUwMDFjc+KZQUd2mkT1Jbuy0L09OvUnav/qqlalo/gw20xDo5aTRcBcdTAwMDVpyy9lVkNMQzNcclx1MDAwN81UXFzOQlCfp5X0a1x1MDAxOepuJK2TSbXnJfmB5+uOY1x1MDAxYZ9YSFxiTkqs35fa8dNM49t//1/9abNhfKaVXHUwMDFk/JivJFx1MDAxZq9ccvF9fFx1MDAwNCmVj0ibNctcdTAwMTR7cj4yXHUwMDFlXHUwMDExWVxc4dJNSTn+jqC0XHUwMDFmboondFx1MDAwNMiSXCKjpXz3p5JcdTAwMTbGrm+j5UBcdTAwMDGRiprFvE+e+fKy3IOPWGqCNEsnSc1cdTAwMWStI0ZpP0lIX5J7xVx1MDAxYd6bz6xHXHUwMDE57y6G6Ij1VsuBjspcdTAwMWG+gf1K6lx1MDAxN/fvXCIp/tCyU8BcdTAwMDTUvZXWnZp7NbZQfnBQksZneGBOyzlvYTRCQ1x1MDAxMSrpt2htUMF781szlHRcYssjN4LeNIbyx/Nv+F5oNvc7jLmX2WA010rPlr3/Nb/f1cr3869vPcru41x1MDAxZs83VKxPWb7sf/7646//XHUwMDA3cfF+/CJ9 + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaVNcdTAwMWLLkv1+f4XD83Gu6tWWVVkv4sVcdTAwMDT7KnZh0MxcdTAwMDShXHUwMDE1XHRcdTAwMWFJaFx1MDAwMcGL+98nU3BRa2lZwlx1MDAxOLc1OMI2ai2lrlOZ52RlZv37jy9fvnafWpWv//zytdIvXHUwMDE1onq5XXj8+ic//lBpd+rNXHUwMDA2XdKD3zvNXrs0eGat2211/vmPf9xcdTAwMTXat5VuKyqUKuKh3ulcdTAwMTWiTrdXrjdFqXn3j3q3ctf5L/77oHBX+VereVfutsXwQzKVcr3bbL98ViWq3FVcdTAwMWHdXHUwMDBlvft/0+9fvvx78HdsdO1KqVtoXFxHlcFcdTAwMGJcdTAwMDaXhlx1MDAwM3TGjT960GxcZlx1MDAwNquUROW1UertXHUwMDE59c46fV63UqbLVVx1MDAxYXNleIVcdTAwMWb6erpfVY3KTf+4tJppn4WbbWhdbVxyP7Zaj6LT7lP0citcbqVar1x1MDAxZFx1MDAxYlSn227eVr7Vy91cdTAwMWF/+tjjb6/rNOkuXGZf1W72rmuNSqcz8ppmq1Cqd5/oMSPfXHUwMDFlfLlcdP/8MnykT79lUFx1MDAwYtBKXHUwMDA1ZVBbo98u8qstorDgvZFGa1x1MDAxOUCZsWGtNSOaXHRcdTAwMWHWf8jBz3BgxULp9ppG1yhcdTAwMGafozVcdTAwMTYrbvicx9cv65yQ1nuw2lx1MDAwMjpcdTAwMTns2zNqlfp1rUtP8VqgXHUwMDAxJ52TWlwi4nCcncpgPrQy1tL14N+u8Me3dspcdTAwMDNs/G/8jjXKr3es0Yui4Yj5wkZcZk/D1/Ra5cLLtCvnXHUwMDAxtFx1MDAwMa+8XHUwMDFm3u+o3rhcdTAwMWR/u6hZulx1MDAxZFwiZfDoX3++XHUwMDAzolx1MDAxOLvpY1x1MDAxMFxyXG6scdqGuVx1MDAxMbqeq97e9FdcdTAwMGXus+bb8Wn17CGfW19JOULBXHT6nt5cdTAwMTmrvfGIwU1i1JhcdTAwMDBGXHUwMDExTI1XLrVcdTAwMTil4dtcdTAwMTCkgyWDaHzRjUFUk21cdTAwMDFcdGjmhmjlbHuX7l6xWLYnR9lcdTAwMWTc2zu6tSmHaEZcdTAwMTkhVUBcYsordIhqXHUwMDE0o+CMsFx1MDAwZSVdUlx1MDAxYe34uNKDUFx1MDAwNcYg/VFLh1CbhFBcdTAwMWFcdTAwMDN5NvTez1xy0YvD4sHeJWCxeXR8V7h42CZ+0/m1XHUwMDEwVfK7ZtRcblx1MDAwZo7uOzpcdTAwMTWcVX5cdTAwMDShzlx1MDAwNEFcdTAwMWMgXHUwMDA0slBcdTAwMTIs6NRCVNOEWVx1MDAxYeqyQdTLZCpKtFx1MDAwNiw6Mz9cdTAwMTW9y+RcdTAwMGJms7SSO83XbivrtaONrUil3YqiXHUwMDEyxEItXHUwMDEyp0ElbcxOXHUwMDBlIGqlIFx1MDAxM2rBKktE1UNqIao8kVx1MDAxNCAnsGxcXNRjXCJcdTAwMTdVXHUwMDEyQGFcdTAwMDA9P1x1MDAxOVxyvuAgat7c9Eo5dVx1MDAxNfXKzUN/kHKMKlxyXCI4XHTk51x0XHUwMDAyIcbNX/w8XHUwMDEwXHUwMDE3ddKjNlaa2PynXHUwMDBlooPXKlwiLMtcdTAwMDbRgMlcdTAwMTA1jr4wefu5Idp+3q2t+I1cdTAwMDNQ29umkll/3jyqnafd09PckqsnXHUwMDEy5zXQv4Cjop6QKzSSlncsXHUwMDFktU4zXHUwMDFilajQS7lsIMXYN5rQ9ETB0Nj5MbpiiqF7t/OMzdr26UbumyqcN9Ku6TNBSGkk3XjNXHUwMDFlP3hcdTAwMWODKFxio1x1MDAwM0hvIfhcdTAwMTRbUeVcdTAwMWSJenqfJVx1MDAwM6iL2chxgNJcdTAwMWTT3s4v6FeDbYZu93a12LnVd66sOpeNVsrxqVx1MDAwMlxiZb2Sylx1MDAxYWI2So/6eeLihFxmSf9Ip8nbK5teiFx1MDAxYUJcdTAwMGZ6evWSQVx1MDAxNH0yXHUwMDE3XHKWXHUwMDE0hJN2fr20XHUwMDE39WvrNZPPdmUl2nCPT1Grf5ZykFx1MDAxYWNcdTAwMDRcdTAwMTFNXHTkxyU6PVx1MDAxZbsnjJKFJVR4abRMcVxclFx1MDAxNlx1MDAxOZFcdTAwMTFcdGbZXHUwMDAyo1x1MDAxZVx1MDAxMv08WVx1MDAxNUSgiZs/7Fx1MDAwNLdcdTAwMWJRsVxcfdg8fVx1MDAwMFnOX0RXjY0o5Vx1MDAxONXOXG6DZICCslx1MDAwZVxiq2N6SVx0JOxazXEpnWIrXG7OK01kdOlcdTAwMTBqXHUwMDEzQ/fKgyFcIoZhfoSu3drLrropX+dcdTAwMGVK7fPHtsnVXG6FtMslNqPWXHUwMDA2XHUwMDE3XGZBgHd9RyDKUSelXHUwMDFket5cdTAwMWNV6FKMUW+M4r+WXHUwMDBlo7Fd6Vx0T+80eVx1MDAwZvDzyyW4udze3fe5w1x1MDAxNVtp6t3D8/NW3qdcdTAwMWSj9D1cdTAwMDWBXHUwMDAzjVx1MDAwYsRJjZlcdTAwMDCpXHUwMDE2JJaIXHUwMDAyaFqxXHUwMDE204tRWmRIVGX5dultoqcn6kXTRoppfsmkt1f8SbRbzz+stW6uXHUwMDBm9V1V3uRT7umtNIJYN8pcdTAwMTBsUN6Oe3riXHUwMDAxmqAhXHJcdTAwMDZNtja9XHUwMDEwdYDkXHUwMDE14yZlSSDqkvdAnSFcdTAwMTOCQc1cdTAwMWa8P+hePdTtbf+8tXqfX+k+ZU9Palx1MDAxYr9cdTAwMDFELSjOaFx1MDAwMnKU2oyqemJ4goDL84+SU05Si1HOhiHY2KWDqE5mo4qMhrbOze/pL/dcdTAwMWa3irV+Nerht51G+LbWbFx1MDAxY16m3dNcdTAwMTPVXHUwMDE0zqMmraRcdTAwMWNcdTAwMGJEP4ZRL6RGnlx1MDAxOXScS5RajGpcdTAwMTlcZtlRXHUwMDA1y1x1MDAxNnjyesZcdTAwMGVcdTAwMTNvViyQ7Fx1MDAxNF0+51x1MDAwZl1cdI7v3GOje7h3d9O+OE65XHUwMDE1zSApXCInXHJ9T8NBmzGAXHUwMDA2KbxyZGbJxGq06dVLXHUwMDE2jFx1MDAwZc7EXCLZy4FP9CFcdTAwMTGfarDvK/38XGK9fixtntc7XG6vck+79Zp1p/mNX1x1MDAxY1x1MDAxOJ0j18mTXHUwMDFjMkhUTnm0XHUwMDE4x8dcdTAwMGJGXHUwMDE18Vx1MDAwMFx1MDAxN2hq6I7ImIVKXHUwMDFiRulcIqKxuGxcdTAwMWKgflx1MDAwNkadXHUwMDAxSYNZYIep0qna0tnZXHUwMDFl7LXWWi6f7dU2ittpt6JKXG5imlx1MDAwMZ3SwDl3scSZp1x1MDAwMVt3gnRcdTAwMTSpfdLMhJ70klF06OM5XHUwMDE1y4FQlIleXktcdTAwMWI4wUvOT0WDjGrdXnHLV/bLdZm9KPUy+82UI5SMqHFBXHUwMDA3IC5cbkHqUUFcdTAwMWasXHUwMDE0wWrSIVZLk+IkXHUwMDEyXHUwMDEy8kRGzPJF7kNivmhcdTAwMDDet1x1MDAwZTC/mi82a+q6vbt397x6dHf90No9vNjIplx1MDAxY59cdTAwMTmPwnvveCeNqCbZ0TGAXHUwMDFhwVx1MDAxMlx1MDAxZr2G4FCnN11US2LTUiu5dEw0OeJEXHUwMDA2xXtcdTAwMGIwv5M/zV5mzm4vXHUwMDBldlx1MDAxZUqXvZ3DSvT4ePiQcogq7YVlXHUwMDEzScuR9LBcdTAwMWR38Vx1MDAwNGBFQp+UiFxyXqU3595rXHUwMDBmYVx0bWiQifEmtEbS7YpccuR7XHUwMDAw3djBx+vzvUb2oXKcW1k9K1/1XHUwMDFhaY+IMkC59jOQXHUwMDAxMlxcXHUwMDE1Mlx1MDAwMlBcIp8gJE2Id1x1MDAwMNbFs/TShlDl+Fx1MDAwZoTlU/OYXHUwMDFjbfJBk+dwXHUwMDBipDk9bD12L052rrPr+cra7s3lbqlWuEg5RjNkQ8k7WpBcYs75MJbOTCD1XHUwMDAyLf1IonokptJrRlx1MDAxNV3wNIKli4hCop93dL+0XG5cdTAwMGJcdTAwMTSF7N+qu25me791f1x1MDAxNG3tZVx1MDAxYX24Okp7vElcdTAwMDXPRfRSak1IRTOWzVx1MDAxY7SwXG5cdTAwMDZld4RiSK9WUoqWkLTLXHUwMDE3XHUwMDEzXHLJKSRcdTAwMGWR5k3Nn+SUr9dUpZnZrDeyuTpkt7ea4fFcdTAwMTdr+XmSnJwg/Fx1MDAwNfBcdTAwMWNQXG7ju0qEUFx1MDAxM1x1MDAxNDJ2NG/apFx1MDAxNqHOaSBFh8tmQuPJj1x1MDAxM5tKpJPAhlx1MDAwNYzoxflxqVx1MDAxOY7L19/qJ7kzgPZze/Vbyt28dryzSYLdWmKl1o3F7JEuW1LypPdccug021DtPTFcdTAwMTFizMtcdTAwMDZRSC5QXHUwMDBl/oWgzc9Ec1x1MDAxN7utrfrO1km2vbNcdTAwMGZn3dNv552098oxXHUwMDA2XHUwMDA0KkkslFxiqcUwqpaCVVx1MDAwMjmzRFx1MDAxYjfIS0wvRI1RwaJbukYkM7LwXHUwMDE077NcZuJcdTAwMTjzU9HVi8b5Q+2qW9SruF19Lq5XXHUwMDFl71NcdTAwMGVRXHUwMDE1UJCY9zJcdTAwMDRwoJRxY1x1MDAxOLWCXHUwMDFjqHaas1x1MDAxMrVJr6BcdTAwMGbIyVx1MDAxNH75mKhKtKJcdTAwMGVcdTAwMDJcdTAwMTeULZAnenSw53tcdTAwMTnzTZ093TRcdTAwMWX9pu5cdTAwMTa3KylHqJWEXHUwMDBl0sE0vVpcdTAwMWGa49F9JXRBOCBNT1x1MDAxY9Xxmk0vQoHEbXCwbKnMITlqT6RGWVxcpGZpo1x1MDAxODXa94fFTVS7qye3W7eP69FJ6lx1MDAxMcpunpio9GSFlFx1MDAxYzWhg6Bo8JKsq1xytFjTmzyiNC1cdTAwMWaJy6jmXHUwMDEzbSjpKFx1MDAwMK4on1x1MDAxYqHrfu8hX99Yf46uo1a9oXDreusu5VxiJespJFx0ds3uQlx1MDAxYol+XHUwMDFjorw1XHUwMDFhjKL16nyc56VcdTAwMGWjllx1MDAwYlSZMS9cdTAwMTlGXHIkbi2ROFxmysSbXHUwMDA2fFx1MDAwZqO1h8LO5j5G/cvi+qbfLeDJXiUpwWlcZmujXGJcdTAwMWTPxnx7VbnQqVVcdTAwMTaAqJ+nryirILCe407jUXuuTnaKayp5YpTDXHUwMDFmym/qtlx1MDAwYo1Oq9AmXGJMwlx1MDAxNMBcbuc80uRjMFx1MDAxMCvlf4MpXHUwMDAxVFxmWkqR7fC0eKY0XHUwMDFks7yPq+3HwfT1wlx1MDAxMFex+W76y+3DS73SOj/tnFfr7dBw2WH3rlx1MDAxMVx1MDAxMFx1MDAxNtrt5uPXtyt//TnrfbNcdTAwMTf7N7LwzVx1MDAxZNROS/KoVexGe7Az3/u+/i9cdTAwMWSLS2HMRE7sinFTXHUwMDFi8t/zx1wiVq6qhe710eFpM1MzxdLm6emWTYpFvG95/YxuftYhgCPJq1xmhLF6QIPCWGXCoJApXHUwMDFlinqP/a9Wq5OrylhcdTAwMTTAuV+aVSYqnFxcVeSYROBCXHUwMDA2LoDTXGJT4lx1MDAxMEFzzS180qq6KJ+v6fvq/UHtMDjXXFx1R9mzXHUwMDBmRb/+XHUwMDFj9Mvk9kGsmpm3zo19X9rea5SK3e7T3lx1MDAwM7qV5/K+7fXTvptcdTAwMDEgjOEulYRtXHUwMDBilijOKPytXHUwMDEyhqy9QmK/I1x1MDAxNd1poz9eXHUwMDE5K+0nblxif1x1MDAxNkJVXCJCPWkmXGZ+gcyvnZXcw2N0m7P5y/31zdWcXfUnpbRDVFx1MDAwNSu0XHUwMDAzb7ivhVTaTiBUKqclUXSu504zQefAfoBPJOifg9HgXHUwMDEyKURQXHUwMDE2QclcdTAwMDXareL5Vva8f++3V9dO91x1MDAxZexFZNa2f3FcdTAwMGa2efaEtfBkfFx1MDAwMtHv4INcdTAwMWZvXHRstTBcXGuIgXx3mpuzXHUwMDA0XHUwMDBiIZBcdTAwMTdYOjNqXWKgQ0uSNI5z8+ZcdTAwMDZp/2nlNipEXHUwMDFiu/mTfj5/XHUwMDFlitE+tt9Dc8czrH4qzZVcdTAwMTIsUVlcZsDZ3GOOnniuJ1x0SVx1MDAxMHWcvvAzaK5cdTAwMTdgaVxmhrvAxYuS3vCpmGtcdTAwMWKlXHLpRtKxKl6q85q0oFFcdTAwMWFjP3C7bVx1MDAxNs3Vuv+01+mo00fbULtPR7vhtN6Zj+bOXHUwMDE0j79cdTAwMTF9rkRRvdWZvqbi4dDJ8DZ5a7dcdTAwMDB7XstsP++cXHUwMDFl5Ez/yHRu8q3Nx8MoKXg4c0l9ntV3xDz4XHUwMDE0XHLgzqrxzoUvNlx1MDAxZoiX0HSQaPM/WFxcXi2AXHUwMDA2PbmkWDfSiqXXW6JG8fM63pZcdTAwMTSSU9B+UJ05mFx1MDAxMzO+olTgtEKDXHUwMDFm2HjzJy2pVEE/+Vx1MDAxNFx1MDAwNHYl5G1cdTAwMTZIg3uS9cvaw65X6nGvX1xyuc1e70KnXHUwMDFi+p4sNZelO4K2XHUwMDFl4Xcv2CfGzt1cdTAwMTSRXHUwMDFiTehcdTAwMWZMdq9cdTAwMTaK5DV+XHUwMDBl+klCoYOPXGZGplx1MDAwMp7WJqe6O1KUVoGeXHUwMDFmn6v9o/OTi+tOI9tunDnI3O1cdTAwMDBuplx1MDAxYp/BXHSymEZLXHUwMDFmpFxyelxmnUagMrRCOVx1MDAwYlx1MDAwZfyPNVBSuog4hY5/XHUwMDA0OjXNo9JcdTAwMWZcdTAwMTnUS1x0XHUwMDFil8kndHCqXHUwMDAyoF6gVeL9zmZ9m5mDv3xu9Fb07aPZ/MUtvuag444wXHUwMDE4zEAzXHUwMDAyeFx1MDAxY8tz55O4gkRDMs5an+L+SUppx7s9n9hA6ZOCXHUwMDFhJjHuRuaTi2P0XHUwMDAy7HbT39d92K0211x1MDAwZdYuuiv1u8re+i/u3D1fVCNcdTAwMThcdTAwMDAk3cjB31Enz+1p+DaQTpPcdCHFSZo0OuNR/o5efnZv+eTG3exz/FwioeHccbZn9n3ufl1urO7tPeRWdWI9W0qcPHhB3JKMJPlZhHhcdTAwMDX0IP1ccjTZUOctXHUwMDAz1OhcdTAwMWarZStVyrZcXJiEpyGma7w03rHYNVx1MDAxMGP9w6hcdTAwMDb3a8RAbIOjK+Bikai/e9Nw0MNcdTAwMWL1cVG311x1MDAwYlMlmNuEg7Osed4h4bpxlYG1rauD43RKsNlcdTAwMTY6OfygXHUwMDAyV+CCWaBcdTAwMGLjaiVTUCc32G19ezKrXHUwMDFi/ep+dj31LIJcdTAwMTiE8NpcdTAwMDGfkMSngIztjXB3JqJafFx1MDAxNJ2zZFx1MDAwNNPb4lx1MDAwZVx1MDAwN0klbuksNGJi7pIja1x1MDAwMUTv5ofoZdRZMZv99fNLmStcdTAwMWM63y1srph0m2hlQThNXCLLKk+Ky+FYijJYXHUwMDExuIux5NxcdTAwMDX8MSVmS65SnVx1MDAxMicgYyBM8EpcdTAwMDXNTaLklPxcbqVcdTAwMDVBVzuSilx1MDAxMm2Id+J7q5ajb1x1MDAwMPCJZyV+llx1MDAxMU0uRVwiXHUwMDAxq1x1MDAxN+rBeHnfPzi+XFwp7Hb3VvfMt2u5tqHSv72sjeBMZOBEedQgx9uEXHUwMDEygtFcdTAwMWFjiErwIWCptaGKY1x1MDAxZN5/ZuOGT8IoJDp6i8py2/r5Mdpdq1x1MDAxZtTqnZpfqdrNZuam7s8rxbRjlFxcPFx1MDAxN1x1MDAxYpFcdTAwMTM33lpC4jhGg+BdTFx1MDAwNVx1MDAxYy2K979LXHUwMDFiRlxyXHUwMDA0hZzLumRcdTAwMTDFkJhcdTAwMDLhPdH7kbMkvpujvH5cdTAwMTZ2noprV1x1MDAwNXv03LooVI5LV0nndqfEz1x1MDAxYnRCa6KiyFx0yPFTzFx1MDAwN1x1MDAxMVdwpJJcdTAwMDLRcSOdkeHH8JmkxZRcdTAwMTGWKCTH80n7uVx1MDAxMCuHXHUwMDFhOvpg+bgxXHUwMDAyhiXD4eLtW4dp9J7bNnxgs+XXXHUwMDBiU8XYdr7uorPGc7dTkJdrx1x1MDAwN1x1MDAxYrf95+ZvKMZwRpI+V1F6s0CPx0u1WSr1c4f1401501x1MDAwYs+bZ3oj6azQ1CxcdTAwMDBcdTAwMTTKaauY7Mqg3CjRdSTUyFxi6OCN1yb8WI5+MtPldtJMcpFwztUgU0w0au5FKWXwclDQMsF0ud00t6/4vGK8XHUwMDBmQ2i30u9OXHUwMDAzp3LJsVxc7oRcdTAwMTRcdTAwMTSZrLnReVxczt5cdTAwMWZen8BW6/qxf5ft3lQ6/indJSSWJFx1MDAxMJeGaFwiXHUwMDExXHUwMDE2LbjR5Fx1MDAxZm9xkL6mpSH2qGdcdTAwMWO2XFxcbrqgXHUwMDBis9E5NftHw1x1MDAxNCyaiUhcdTAwMThcdTAwMTltXHUwMDEz4LNcImE/qzjkfPPYlTKlQpRvm35/LZ/L9/TpLzPqQ2Q2XHUwMDFi3dP68yDnXHJHXHUwMDFl3Szc1aOnXHUwMDExdFxy1lx1MDAxMlxy8KHe7vZcbtFVh14oXmcuNv2dXG5ccmDwjjDy0pWofs1L72tUqY6uyW6dbszb5W4ztlxyUqKhXHUwMDE06O3aO+Xxr9Rs16/rjUJ0NmNYM23Dy42eYlx1MDAxY1xmJFx1MDAxN8BcdTAwMTCRQJLBbv7NyNmQ+lx1MDAwNZmB37VcciR4hbVOST5cdTAwMWXa6nhiwOD1XHUwMDA2XHUwMDA0Yc5KOyhcdTAwMGYwyds877ZcckZIMjpcdTAwMWGZXCL6XHUwMDAwckpmoOZsKms54cSCJn5cdTAwMTaT4a+8XHJ5L1x1MDAxZFGm02/F7lqh3V2tN8r1xjVdXHUwMDFjWo+vlZeP3pnDyfCXK7ReOK/BQSDRXHUwMDA0XHUwMDA3xlxy78tgZZd6/EUyUlx1MDAxMPHmU83o/jJBRv36pDcz9rXSKH9/ULOLJ99cdTAwMDZllFxiViHNXHUwMDA2KFx1MDAxM+tcdTAwMTdcdTAwMTZcdTAwMWaSXHUwMDEySlx1MDAxOUtiXHUwMDE2XGZxdU5cdTAwMTWaXHUwMDE4UlTodNead3f1Lt36o2a90Vx1MDAxZL/Fg3u5wlx1MDAwYrtWKUyYXGb6SvFr41x1MDAxNqDF7zjqXCKG//syXFwkg1/e/v+/f059diZcdTAwMTHDg6uT8Fx1MDAxZL7hXHUwMDFm8X9cdTAwMTe2Xsonx+8kLVry5zHy8z3rNdtxpdJ6KS3QOrJbhGtcdTAwMWJvXHUwMDE19LJcdTAwMDFC64NjXHUwMDE2XHUwMDEySPU5TN5cdTAwMDR8P7FcdTAwMTGBXHUwMDA0XHUwMDBlcptcclx1MDAwMrKOXHUwMDA1iIfGKzhcdTAwMTH0YEOKlIGKPeUtiUJcdTAwMDWn0Oj/J7brbysgXHUwMDA1YjDG6YCKT01cdTAwMWJcdTAwMWVcdTAwMTT3ZktQOJo3I1x1MDAxNVwiS3ZcdTAwMTdmW67Rb/E7XHUwMDE5kERcdTAwMWPxz1x1MDAwNIJcdTAwMTY0XHUwMDFmScJIJ1x1MDAxZlx1MDAxZWJg4PRcdTAwMTdoUNLMPX27881Kq5S96Fx1MDAxY9p+gMz5uzJcYj5PXHUwMDE3YVx1MDAxMHS7yWxbIM0s5WhcdTAwMDZcdTAwMDHXO1xuy3WPpO6NsTPiqu83XHUwMDFmdi5dxCm2XHUwMDFl8Fx1MDAwM89cdTAwMDZ5Q9DHXHUwMDE2t8/URVf3xO7v1srX61fkl7bPjlx1MDAwZXdKtWXQRS+zmTZZ9DKq9/GKeDbq+Ma1I7bpXHUwMDEw54+YzMZTXHUwMDFhaYWxROj4XHUwMDE4a85xXHUwMDA3r/W4ZdBEO8gpWe79Z2adKfRuy6CkXHUwMDEzSFx1MDAxZk0kmlx1MDAxMG7JXHUwMDE2T1x1MDAxYVxuJZw2xH5cXOBunyOnjv5cdTAwMWRQ8STrTDp3XHUwMDA0XHUwMDE35Vx1MDAxNbNcdTAwMWTMXHUwMDFiZVxiQlx1MDAwN1x1MDAxYYjj4XB3vqn6Q1xuyZEwTpuRnL1qzd8+9edoXCKNQmlOLSA/41xyn1x1MDAwNDRdXHUwMDE1ySB5XHUwMDEzw3BKXHUwMDA0uN9cXFx1MDAxNSVDmH8yk+hdkNYkW6/kI6dcdTAwMTTdYD4+XHUwMDE55t+OmO220mi+vFx1MDAxMazypfWM7ripfrVeXniUXHUwMDA2XHUwMDFj60NcdTAwMDXJeWHvt15AXG5cdTAwMWZcdTAwMWTjXHUwMDE4XHUwMDFkn+Q5bTNOOC8t+uBcZveopIFMWC/ChVxun3mA9C+1XzHrpCxHXHUwMDFlJWhWSCHWyfLNonDvPO2tNsRcdTAwMGJfOsfMNmC/rzRKxtLL5Vx0XHUwMDE4LWhGktSR0cl9XHKc9U5cdTAwMDU1f2jlqLb12I/y2aen5zUwK1lcdTAwMGKN6vqvNFwi4XtGJGOEJttgvLKaO+PhWFx1MDAxZnEwdOctcVx1MDAxZnRcdTAwMDGdXHUwMDBmyZ03fro6XHUwMDFhXHUwMDFjV1x1MDAxMz5SXHUwMDFlpUdokJSoik6p3Yyiq2a12qmkYv9lyqje56qNSc7iRlx1MDAxM3RcYvFcdTAwMWVN31tls8uL0+iqM1x1MDAxZYQyoFxm52gr4+xo4oBccnKwc0ve3HJTuJ+wyJRWwjpuXHJBJnTQMHiKq3ZGeD6J2nuJ0pnJXHUwMDBlZJpcdTAwMGY78fYzj+T5XGJfPe7Vvu/FZzcqiHtxy90sUHuliNwy65zmw1x04jS50lx1MDAxYjCGhMHvzfdcdTAwMTORNLg6gaFcdTAwMGby0zqGhHFcdTAwMTNiXHUwMDEwnEK5QFx1MDAxOPPk2+1h4/rhvH7Tu8xcdTAwMTbAnJXVbpRuR01cYlwi4cjnXHUwMDEw8HnPwY9cdTAwMDYrXGJ/QkmQLtD61Mn5y3NcdTAwMTiQmd1BlZtiOKY4a1KEgdjCXHUwMDA3ptK/4Sj1vVZ+jFxuPH3515dXn/uUXHUwMDA2XHUwMDEyMDKe97l/mJF94Vx1MDAxMYFInVlgXHUwMDBiYuZkp3LtKmmFRVI33rjByYvjqd1GgOSFTSpQk977ecuXXHUwMDBmXSFZwz1cdTAwMTFAWaunKHYrhaNRKkDHXHUwMDFicmFcItyovFx1MDAxY5zO6pZjJ3O2L/hcdTAwMTLfydRcdTAwMDEtd4iXdO+k1zHP97evt4KTbGzgXHUwMDE2rlx1MDAwNpx/Z1x1MDAxMsbsXHUwMDE2uyNjQq04uKkgeI5cdTAwMDW5yTF58lx1MDAxYtrymc4k9bjE5ffmXHUwMDFmiVx1MDAxMOafzFx1MDAwNHo/iH+ATORcdTAwMWbacXHwXHUwMDAyuf/6KNNej45XQj/KbeXqd7t5XHUwMDFharotmCegcUpcdTAwMGJcdTAwMWEksWZh7JxcdTAwMWNnheJGpTQxge78XHUwMDBm7aHONmAxUzCTf1xmjkb7wD2RWfQjXcXWP0Y/+kN3309cdTAwMDP9XHUwMDE4XHUwMDE5z/voR0imXHUwMDFmoIF7XHLi/Gt39lxcp3HtqsDcXHUwMDAzjFx1MDAwZsqSPYTRXHUwMDAwXHUwMDFmXHUwMDE5USFJuVx1MDAwMlx1MDAxOVN6woyTXHUwMDAyy6Q9ZPX9S9c44YPUJIWBXHUwMDFjKIQpzZKUJDOj2HCDJlx1MDAxNaPCZFx1MDAwNqhX7Fg/Mnn8V3KP2Z7gy+heJoRgOFxiSk7eklx1MDAwMnSTrj5cYj5cdTAwMDLMXHUwMDE4Pi7Dqu/sXHUwMDE1JI1pdpOQ2Ji474CWSiMqLiG2XHUwMDEzw5ncbf2diEZcIl75JzNcdNVcdTAwMGZcIlx1MDAxYVYlXHUwMDE2WVx1MDAxMes2ltu4z1x1MDAxZivdcFtr/d55XHUwMDFlL+Xp7W29XHUwMDA251Eu3VRDXHUwMDFizYeL8GntXGauXHUwMDAwo1qJXHUwMDA0XHUwMDEy0XCliYrzcVxi8udpJT0tR91P5HVcdTAwMTKpRlqTXHUwMDFmeMTuLKqRrlLCj6NcdTAwMWFf/vN/XHUwMDFhL9tccrNTrdzo23wm+5g2xPdcdTAwMTFcdTAwMTKDyYXE0nHXwlx1MDAwNXo6zUZEXHUwMDFhVzj3U5JcdTAwMWWDV1LjeFc85iNcblx1MDAxY0iwmlx1MDAwYnh/KG1h5vq2ms9cdTAwMTRgqahJzGM8WeRtuVx1MDAwN1x1MDAxNCQ1XHUwMDE190tcdTAwMDfOzZ2sJDbcf1x1MDAxMlxmLEf21Wx3McZHXHUwMDFjOs1HOkpn6Vx1MDAwNlx1MDAwZWup3/y/XHUwMDE3XFz+oXmrgFx1MDAxOKj/Xl53YvbVzFL50UFxXCKfpYF5zSe9hclcYlxyXGIjueGic0FcdTAwMDZE+1tTlGRcYvNPZlx1MDAwMr1JXHUwMDE05Y/XT/haaLVOu4S5t9kgNNfLr5Z9+DW/PtQrj6vT91x1MDAxZXn78Y/XXHUwMDFiytanwl/233/98df/XHUwMDAx7Fxyf1x1MDAxNSJ9 From 8540900d3b5d7e6e53ba456ee63746a84a117f08 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 4 Feb 2023 17:16:00 +0100 Subject: [PATCH 12/17] add test for extend_cell_length --- tests/test_strip.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_strip.py b/tests/test_strip.py index 0dc082c2b..40f3975fe 100644 --- a/tests/test_strip.py +++ b/tests/test_strip.py @@ -83,6 +83,14 @@ def test_adjust_cell_length(): ) +def test_extend_cell_length(): + strip = Strip([Segment("foo"), Segment("bar")]) + assert strip.extend_cell_length(3).text == "foobar" + assert strip.extend_cell_length(6).text == "foobar" + assert strip.extend_cell_length(7).text == "foobar " + assert strip.extend_cell_length(9).text == "foobar " + + def test_simplify(): assert Strip([Segment("foo"), Segment("bar")]).simplify() == Strip( [Segment("foobar")] From e81ac17c6c947b3f28a4b5b71addfd8b93e9718d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 4 Feb 2023 17:35:09 +0100 Subject: [PATCH 13/17] revised copy --- docs/guide/widgets.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 9470d123f..947bda5df 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -310,10 +310,10 @@ The `render_line` method calls [get_component_rich_style][textual.widget.Widget. A Line API widget can be made to scroll by extending the [ScrollView][textual.scroll_view.ScrollView] class (rather than `Widget`). The `ScrollView` class will do most of the work, but we will need to manage the following details: -1. The ScrollView class requires a *virtual size* which is the size of the scrollable content, and should be set via the `virtual_size` property. If this is larger than the widget then Textual will add scrollbars. -2. We need to update the `render_line` method to generate strips according to the current position of the scrollbars. +1. The ScrollView class requires a *virtual size*, which is the size of the scrollable content and should be set via the `virtual_size` property. If this is larger than the widget then Textual will add scrollbars. +2. We need to update the `render_line` method to generate strips for the visible area of the widget, taking in to account the current position of the scrollbars. -Lets add scrolling to our checkerboard example. A standard 8 x 8 board isn't really sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares. +Lets add scrolling to our checkerboard example. A standard 8 x 8 board isn't sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares. === "checker03.py" @@ -326,9 +326,9 @@ Lets add scrolling to our checkerboard example. A standard 8 x 8 board isn't rea ```{.textual path="docs/examples/guide/widgets/checker03.py"} ``` -The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). You can update the `virtual_size` attribute dynamically as required, but our checkerboard isn't going to change size. +The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). You can update the `virtual_size` attribute dynamically as required, but our checkerboard isn't going to change size so we only need to set it once. -The `render_line` method gets the *scroll offset* which us an [Offset][textual.geometry.Offset] containing the current position of the scrollbars. We add `scroll_offset.y` to the `y` argument because `y` is relative to the top of the widget, and we need the line relative to the scrollable content. +The `render_line` method gets the *scroll offset* which is an [Offset][textual.geometry.Offset] containing the current position of the scrollbars. We add `scroll_offset.y` to the `y` argument because `y` is relative to the top of the widget, and we need a Y coordinate relative to the scrollable content. We also need to compensate for the position of the horizontal scrollbar. This is done in the call to `strip.crop` which *crops* the strip to the visible area between `scroll_x` and `scroll_x + self.size.width`. @@ -342,7 +342,6 @@ We also need to compensate for the position of the horizontal scrollbar. This is ### Region updates -When you call the [refresh][textual.widget.Widget.refresh] method it will refresh the entire widget. The Line API makes it possible to refresh parts of a widget, as small as a single character. Refreshing smaller regions makes updates more efficient, and keeps your widget feeling responsive. @@ -360,14 +359,15 @@ Here's the code: ```{.textual path="docs/examples/guide/widgets/checker04.py"} ``` -We've added a style to the checkerboard which is the color of the highlighted square, with a default of "darkred". We will need this when we come to render the highlighted square. +We've added a style to the checkerboard which is the color of the highlighted square, with a default of "darkred". +We will need this when we come to render the highlighted square. We've also added a reactive variable called `cursor_square` which will hold the coordinate of the square underneath the mouse. Note that we have used [var][textual.reactive.var] which gives as reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor. The `on_mouse_move` handler takes the mouse coordinates from the [MouseMove][textual.events.MouseMove] object and calculates the coordinate of the square underneath the mouse. There's a little math here, so let's break it down. - The event contains the coordinates of the mouse relative to the top left of the widget, but we need the coordinate relative to the top left of board which depends on the position of the scrollbars. -We can make this conversion by adding `event.offset` to `self.scroll_offset`. +We can perform this conversion by adding `self.scroll_offset` to `event.offset`. - Once we have the board coordinate underneath the mouse we divide the x coordinate by 8 and divide the y coordinate by 4 to give us the coordinate of a square. If the cursor square coordinate calculated in `on_mouse_move` changes, Textual will call `watch_cursor_square` with the previous coordinate and new coordinate of the square. This method works out the regions of the widget to update and essentially does the reverse of the steps we took to go from mouse coordinates to square coordinates. @@ -380,3 +380,11 @@ The `get_square_region` function calculates a [Region][textual.geometry.Region] The final step is to update the `render_line` method to use the cursor style when rendering the square underneath the mouse. You should find that if you move the mouse over the widget now, it will highlight the square underneath the mouse pointer in red. + +### Line API examples + +The following builtin widgets use the Line API. If you are building advanced widgets, it may be worth looking through the code for inspiration! + +- [DataTable](https://github.com/Textualize/textual/blob/main/src/textual/widgets/_data_table.py) +- [TextLog](https://github.com/Textualize/textual/blob/main/src/textual/widgets/_text_log.py) +- [Tree](https://github.com/Textualize/textual/blob/main/src/textual/widgets/_tree.py) From f56823a7335c84d10f8353ad33b0475527c23438 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 4 Feb 2023 17:47:49 +0100 Subject: [PATCH 14/17] better diagram --- docs/images/scroll_view.excalidraw.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/images/scroll_view.excalidraw.svg b/docs/images/scroll_view.excalidraw.svg index e06ee4634..e2fc7f3f3 100644 --- a/docs/images/scroll_view.excalidraw.svg +++ b/docs/images/scroll_view.excalidraw.svg @@ -1,6 +1,6 @@ - + - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaXPiypL9fn9FR8/HuehV1pJV9VwiXkx4wyve8cLMhIPVYMuAQdjYL+5/n0zsa8RcIlx1MDAxYdput8zQXHUwMDExbVx1MDAxYiEopFOZ52RlZv37j2/fvkdP7er3f377Xu2Xi2Gj0ik+fv+Tn3+odrqNVpNcdTAwMGXJwd/dVq9TXHUwMDFlvLJcdTAwMWVF7e4///GPu2Lntlx1MDAxYbXDYrlcdTAwMWE8NLq9YtiNepVGKyi37v7RiKp33f/i//eLd9V/tVt3lahcdTAwMTNcZj8kU600olbn5bOqYfWu2oy69O7/TX9/+/bvwf+x0XWq5ajYvFx1MDAwZauDXHUwMDEzXHUwMDA2h4ZcdTAwMDNELcaf3W81XHUwMDA3g1x1MDAwNXTSoLFcdTAwMDbeXtHortPnRdVcblx1MDAxZK7RmKvDI/zU95O9XHUwMDFhNKs3/aPyaqZz6m+2TPtqc/ixtUZcdTAwMTieRE/hy6Uoluu9TmxQ3ajTuq2eNypRnT997Pm387otulxuw7M6rd51vVntdkfOabWL5Ub0RM+p4fd7uVxi//w2fKZPf2WcXGaMXHUwMDA08KCc1Eq+XHUwMDFk5LO1c4E21iqhpFx1MDAxNN6AXHUwMDFhXHUwMDFi1lorpDtBw/pcdTAwMGYxeFxmXHUwMDA3ViqWb69pdM3K8DVSulJcdTAwMTWHr3l8/bKIgdDWXHUwMDFhLbVxKLx+e0W92riuR/RcdTAwMTIrXHUwMDAzp1xmXG5EIYVzbjjObnVwPyQorem4t29H+OPb25VcdTAwMDE2/jd+xZqV1yvW7IXhcMR8YCOGp+E5vXal+HLbXHUwMDAxrTGSXHUwMDA3apx/O1x1MDAxZTaat+NvXHUwMDE3tsq3Q6RcZp7968+fgKhcdTAwMDObXGJR6b31XHUwMDFlcH6Irudrtzf9lf37nDo/OqmdPlx1MDAxNPLrKymHqMFcdTAwMDCMtqi0tMo653FcdTAwMTKkSnmjgHCqLGBqQUrD195cdTAwMGI0S4ZRKyBcdTAwMTGjViqh6ZKouTFaPd3aoctXKlX08WFu2+3uXHUwMDFl3uqUYzRcdTAwMDMqXHUwMDEw4J3xYMGhczBcblKDKtDoXHUwMDA0XHUwMDFkXHUwMDAy6fT4uNJcdTAwMDNRMEo5+lx1MDAwN0tcdTAwMDZR9C5cdKLOaDBcdTAwMDTQ+Vx1MDAxMXpxUNrfvTSu1Do8uitePGxcdTAwMTHB6f5ehIL4oVx1MDAxOdWBJUJjpEPwqGNuhc9H5Vx1MDAwM1wiXHUwMDAx5FC0XHUwMDE3Rlx1MDAxYplahEqwVtNQl1xyoVx1MDAxNpK5qDZIvo+My9xcdTAwMTC9y1x1MDAxNIoqW17Jn1x1MDAxNOq31fX64cZmXGJpN6JcdTAwMGVcdTAwMDKiodoplFx1MDAwZYSOmcmnXHUwMDE3slx1MDAxZZBcdTAwMDXVNF01MVVrUlx1MDAwYlGwRFJcZvmAZSOj1iWSUSnI/4Ej4zE3Rr0tolx0Wzc3vXJcdTAwMWWuwl6ldWD3U45RkCbwKFxmuXmCgI8polx1MDAxNzdviIuisE4qLVTs/qdcdTAwMGWig3MhblKWXHUwMDAzok7IJIhcdTAwMDLdXHUwMDE39GJ+P9953qmv2I19XHUwMDAzW1uqmll/zlx1MDAxZdbP0u7n6c6SoydcdTAwMDZnpaGfxo1qesJtIFx1MDAxZEl5RKm9lGmmosKBs0IsXHUwMDFkRFWyp1x1MDAxN0JcdTAwMTlDZtTOXHLSXHUwMDE1VfLR3faza9W3Tjby51A8a6Zd0mdAXHUwMDA2iiNLpOtcdTAwMWR5XHUwMDBlXHUwMDAy41x1MDAxOEhccrFRIaX1ZKPQpzfuXHUwMDA0YJFkPb3PkoFcdTAwMTS9T1x1MDAwNCmi9lbCXHUwMDAyodFVr1s+im5XS91beYdcdTAwMTXoXjbbKVx1MDAwNyl4XHUwMDAyoSZaXHUwMDBlWlx1MDAwMXl8OerrtUPChqBcdTAwMWZcdTAwMDIleVx1MDAxNtDpXHUwMDA1qVwi/DhLZy9cdTAwMTlInU3mo544XHUwMDFh6dj5MbpcdTAwMWL26+t1VchFolx1MDAxYW7g41PY7p+mXHUwMDFjo0qpgLimIH2ohUM5XHUwMDFlvyeIXG60XHUwMDA0XG4rlFx1MDAxNClcdTAwMGWN0lx1MDAxYyMnIIxautioMUlcdTAwMTBcdTAwMDVPqp7unvFzY9TcboSlSu0he/JgRKVwXHUwMDExXjU3wpRjVKJcdTAwMGWUI/vjQaNRMYH4XCKZIHCEXS05NCVTbERcclqQxEiXXHUwMDBmoYnRe1RI31x1MDAxN+z8bHTtVl9GcFO5zu+XO2ePXHUwMDFkla9cdTAwMTeLaZdMbEW19sQzXHRcdTAwMDFcdTAwMDDCjSCU404g0VleXHUwMDFmXHUwMDA1hymGqFVcbvi/T4OoXHUwMDE3n1x1MDAwM1GFSVx1MDAxMCVcdTAwMGJK1Mzg/Fx1MDAxMDU3l1s7ezZ/sKKrLblzcHbWLti0Q5R8RUDYcFxuPVx1MDAxMVKlJjAqXHUwMDAzIyxcdTAwMTFcdTAwMDBJ2km79EKU5pgjovJ56/SfXHUwMDA0UadcdTAwMTP9vFNSL2ZF5daKPVx1MDAwZXdcdTAwMWGFh7X2zfWBvKuJm0LK3bxcdTAwMTYqQOmc8F57pt5jbp5IXHUwMDAwTVPS+s7LVCt6NM5qa+yyXHUwMDE50fh3XHUwMDFkZ6JcdTAwMDROT994gVx1MDAwNab96OqhoW/7Z+3V+8JK9JQ7Oa5vfFx1MDAwNYiisMRFjXNqLHjPQSfiqk5cdTAwMTBcdTAwMTVgtZ9escTJMNaCXjqEqkQmXG7gnFbxVetcdTAwMWZcdTAwMDH0cu9xs1Tv18KeO99u+vO1VvPgMu1unmhmgKRcdEkmXHUwMDAxOrr6dlxmojZcdTAwMTDS8Y1xyJlEqYWoXHUwMDE0XpFcdTAwMTVcdTAwMDXzaSGnT6OiiVbUXCKQXVx1MDAwMT+/nFx1MDAwZi+fXHUwMDBiXHUwMDA3WDZHd/jYjFx1MDAwZXbvbjpcdTAwMTdHKTeiXHUwMDE5R3JcYoVcdTAwMDKnXHUwMDE0XHUwMDA3bMZcdTAwMTDqRWBcdTAwMDG1XHUwMDAxXCI80un0iiVtlPSotFoygJJjSzSiXGJGkP2w8y+BXj+Ws2eNLrir/NNOo67xpLDxm4Oic6Q6WVx1MDAxMkPKXHUwMDExk1x1MDAwM+vIocfw8YJRXGK0QU+3XHUwMDA2SO3HTFTaMEpcdTAwMDfpdmr3aSugn2VEZ2DUe28txlx1MDAwM8E/TFx1MDAxOO3WdPn0dNfsttfaWMj16lx1MDAxYqWttFtRXHUwMDEwXHUwMDAxXHRccu9cdTAwMTCk4ZQ76Ucw6lxiPiSjSOuTYib0pDeTxKGzwi9cdTAwMWRcdTAwMTWFZLHkpOLlXG63QKaTXGLrUa+0aat7lYbIXZR7mb1Wylx1MDAxMUpGVKGX3lx1MDAxMFx1MDAxOaWrLkf1vNdcIvBaklx1MDAwZdFSqFx1MDAxNGeRkKolMqI+L2r/WVx1MDAwMI15wck8J621lH5+gJZadbju7OzePa9cdTAwMWXeXT+0d1x1MDAwZS42cilcdTAwMDdoxrrAkqsgXHUwMDBm7ohrXCLIMYSqQEmaplZcdTAwMWHPxVxcqYWoXHUwMDE0RKeFXHUwMDA0sXRUXHUwMDE0XHUwMDEzk+6NRK28WCDP6SR3mTm9vdjffihf9rZcdTAwMGaq4ePjwUPKIVxu0lx1MDAwNpptJPlcdTAwMGJSxHrcx1x1MDAxM4CBpD5JXHUwMDEx7S2kN+feSo5cci6fXHUwMDExjVdcdTAwMTGMXHUwMDAzXHUwMDE0LWptXHUwMDE2yFx1MDAxNt3Ydo/XZ7vN3EP1KL+yelq56jXTXHUwMDFlXHUwMDExZYBy8acnXHUwMDAzpLgqZFx1MDAwNKDEPk0glGE+bjR6m+J1JeR/xi+fnPcz8pml8srH5eGPMPqw+Vx1MDAxOF1cdTAwMWNvX+fWXHUwMDBi1bWdm8udcr14kXKMZsiGknekuehcZqL1Y1x0zVx1MDAwNFJcdTAwMWI4XCI8WnC1tkxx6Vx1MDAxMtBcdTAwMDFLI1i6mKhJ9PPe83KKm9+K+r1buIsyW3vt+8NwczfT7Jurw7RcdTAwMDecwFuuolx1MDAxN0JKQqpTo4vz1suAS1xmueyOUGzSK5ZcdTAwMDBoXG5cdL18QVGfnEBiyX5YsqLzi6VCo1x1MDAwZdVWJtto5vJccpPb2mz5x9+s5ufJccKAXHUwMDAw6I3lkJJcdTAwMWZfWFwiiCpcdTAwMGaO74nkdZvUQlx1MDAxNFFcdTAwMWGSdG7ZbKiTyTZUKq5cbtFcdTAwMGJcdTAwMTQon1x1MDAxZJVb/qhyfd44zp9cdTAwMWHTee6snqfczUvktU1cdTAwMTLsWlx1MDAxMysltjlcdTAwMWG0d3RYk5Inva+MTLNccpXWXHUwMDEyXHUwMDEzQbNsYokuSFx1MDAxMkKl4JpxWETO5y922puN7c3jXFxne8+cRifnZ920N8tRylx1MDAwNFx1MDAwZVx1MDAwNLFQXCKk2vlRteQ1XHUwMDA0ztpB6lxip3anXHUwMDE4okpcdTAwMDHJXHUwMDA2/LxGJClIwkMpwNlcdTAwMDWMqF+9aJ491K+iklxcdVu159J69fE+5VxiXHUwMDA171x1MDAwMtLyVnhv0Fx1MDAwMChcdTAwMWODqFx1MDAwZch9SiSeXG5KqvTqeU/zzFxuu3xEVCZcdTAwMWFRh8pYb1x1MDAxNyigP9zftb2MOofTp5vmo83KqLRVTTlCtSB0kFxmptsrhVx1MDAxMnasXHUwMDFjxKFcdTAwMGbQXHUwMDEwJSeGirw2ml6EXHUwMDFh1NKjWbZEZp9cdTAwMWO0J9cnnCb2Pb+b3yiFzc79QSnrYGf1+Hbz9nE9PE49RNnNXHUwMDEzXHUwMDEzXHUwMDE1lsxcdTAwMTCIUVx1MDAxYjpcYorSNCXzqj0qld7sXHUwMDExkDR/hFtCNT+jbaM3ZiBcdTAwMTDmh+i63X0oNDbWn8PrsN1ogtu83rxLOUTJflx1MDAwNoJcdTAwMDS7pNtPflxcODuOUV5cdTAwMWL1XG7oaqCNXHUwMDEzvdSBVHN5KlPmJVx1MDAwM2n8XHUwMDFijXt6jsGAXyDbvv5Q3M7uubB/WVrP2p2iO96tJqU4jWFtXHUwMDE0oeP5mG9nVYrdenVcdTAwMDGI2nk6i7JcZjLactxpPGzPtclcYlxcUck3XHUwMDA20L0rwynqXHUwMDE0m912sUNcdTAwMTCYhKkxOkC0zlhwnmyDmoQpXHUwMDAxNFx1MDAxOPSUMobIM0zrOqZ5IVfqj4Pp64EhrmL3u2Uvt1x1MDAwZS7lSvvspHtWa3R8XHUwMDEzc8P2XSMgLHY6rcfvb0f++nPW++Yu9m5E8Vx1MDAxY/frJ2Vx2C5F4a7Znu99X39Lx+RcdTAwMDKSc0mzXHUwMDBiPVFLTlwinHt2rVxc1YrR9eHBSStTV6Vy9uRkUyfFXCJ+bnb9im5+XHUwMDFhnTHoNKk848fKXHUwMDAxlVx1MDAwYpRcdTAwMDblXHUwMDA3hUzxUNT43FKoa2U72/zXarXJSaW0XHUwMDBiXGYnf0mWmVx1MDAwZdzkpFwiv1x1MDAxNHguZSCtidKZKfWqaFx1MDAxNNerflx1MDAxY4eeOakuKmdr8r52v18/8IitVTzMnX5F8Fx1MDAwYpFcdTAwMTjmUJ5r7DzMX4Ngy1u7zXIpip52XHUwMDFmXHUwMDFjrjxX9nSvn/bVXGZjXHUwMDAypbhNJSFIXHUwMDFiTVx1MDAxNGdcdTAwMTT/XHUwMDFhXHUwMDAyRdZcdTAwMWVcdTAwMWPR35GC7rTRXHUwMDFmXHUwMDBiSlx1MDAwYr10K8Isklx1MDAxMu2ztFxuuVXz3Fx1MDAxMN1eyT88hrd5XbjcW8+u5vWqPS6nXHUwMDFkouB1INFYxV0tXHUwMDA0SD2BUFx1MDAwMShcdTAwMDVRdK7nTjNB59RcdTAwMGIyK8tG0H2sk8NE12pLs9LoXHUwMDA1Mmjd2WburH9vt1bXTnZcdTAwMWb0RajWtn5zXHUwMDFitnlcdTAwMTaFZUDflHvIO2+9XHUwMDFkb1xurGWguN7QefLeae7N4rXxntzA0tnR+FL8RFx1MDAxZIKXPDlcdTAwMTdYdOs/rdyGxXBjp3DcL1x1MDAxNM58KdxznZ8huuM5Vr+U6PLiXCKRWedccudzj3l6YrooyMrSYVx1MDAwZU2+r1wiNoHp2sBoXHUwMDFhhOImcPFl+DeAXHUwMDAy021cdTAwMDVSkXIkJVx1MDAwYvFynde0XHUwMDA16cjj6Vx1MDAwZlxccZvFdKXsP+12u3DyqJuw83S4409cdTAwMWHd+ZjuTPn4hVx1MDAxOHQ1XGZcdTAwMWLt7vRJpXRi2zivXGZcdTAwMTdeL1DDu5bZet4+2c+r/qHq3lx1MDAxNNrZx4MwKXw4c059ntlH4lx1MDAxZbyRhuH2qvHGhS9G31x1MDAxMDORinWbfWeFea1opJGTU4qlo0VH52tcIkfxLTveppQjryDtoEJz0LRejc8o8Fx1MDAwNFx1MDAxOW5cdTAwMTf+OeLxXHUwMDFkUypV0E/eXHUwMDA3gXxcdN1cdJLq82P/STQu61x1MDAwZjtcdTAwMTbgcbdf8/lsr3ch0419S6aaa9ORsM0lN+M95om0cztF51x1MDAxNd2Wdya814olclx1MDAxYr9cdTAwMDb+ljxcdTAwMWaaj4xHplx1MDAwMp/xnScm+I4hXHUwMDA361HMz3dW+4dnx1x1MDAxN9fdZq7TPEWTuds2LptufHpcZshkKimsXHUwMDE32ssxdKrA0Vx1MDAwNFx1MDAwNcuZcMa+r4lcdTAwMTLIknNTXGL5R6BTau1gpEPwl0Hnj+JcdTAwMWGJS4/ouVx1MDAxMHFcdTAwMTHzeb+dbWwxdbCXz83eirx9VNnf3OZrXHUwMDBlQo6EQa9cdTAwMDaq0Vx1MDAxOKJcdTAwMTEjXHUwMDEwtbxcdTAwMWKXXHUwMDE3TpGQ09rq9NatXHUwMDAzSORcdTAwMDWfpeui5FVyx1malMT8XHUwMDE2yYLL2vuG9Tu11tr+2kW00rir7q7/5s7d88U16DJcdTAwMThHypHjv6NOnnvUcJ1cdTAwMTBcdDXBnVx1MDAxN1KcqEmjU9aJr+jlZ3eXT27cXHLo0SiM5yz8MJf4KNdTezZ/vy42Vnd3XHUwMDFm8qsysaotJW7e2IDYJZlJ8rSOV51HXHUwMDEwSkyHrChazVx1MDAxMFXyfVx1MDAxNW3lakVXipNcdTAwMDBVxHWVXHUwMDE1yqJEy7rXTlwiXHUwMDE0uGuj88Q3aJzeYGyfhr9b1HDcwyr4uMjb64GpKlxms2b/NKeet0m7blxcZcza5tX+UTpV2Fx1MDAwZjKYkvGvrPfct2B+XHUwMDFivVrNXHUwMDE04fjGRe3zJ7W60a/t5dZTzyOIQ1x1MDAwNFai4X2SXHUwMDA09z+YbNIkXHUwMDA08oZ0qMlcZqa31Z1cdTAwMWJkluDS2eh4fcPE+lxiXHUwMDExXVx1MDAxMHaBXHUwMDFji8uwu6Ky/fWzS5EvXHUwMDFloI2K2Vx1MDAxNZVuXHUwMDFiTWI0QEk6S4Ml0YVuLFXZ6MBzL2NhXHUwMDEwjHufXHUwMDE402Ws1qaEXG7A+0B5XHUwMDBi4CU3i1x1MDAxMlPSLEBcdTAwMDaEXYmkXHUwMDE2hdM+3pHvrWaOvoExn7dl4mdZUZsoxrSw0lxi1Fx1MDAwYlx1MDAwMPS+v390uVLciXZXd9X5tVjbgPQvMktcdTAwMTNcdTAwMDDhXHUwMDEzPHd9Jyc9vtlcdTAwMTdcdTAwMDaKXHUwMDEzJTSpMVx1MDAxZs+zSZtcdTAwMTFcdTAwMDVcdTAwMGV3WLt8/Vx1MDAxYrxN3EdJKbpcdTAwMWXWLFB5XHUwMDFjrTX2641u3a7UdLaVuWnYs2op7Vx1MDAxOJWoXHUwMDAyIySZJlx1MDAwNHA40XVcdTAwMTnJimryoVx1MDAxMuhHilva8v6rjlx1MDAxM1qXXGahzifnQXB4xHPzn7khWl8/9dtPpbWroj58bl9cdTAwMTSrR+WrpN27U+LnlcNASuKijtOQ0Y8lQVx1MDAxOLKhnErvaboq4d9X75EkxkBcdTAwMDWaOCSH9GkmYHxcdTAwMWLVoaP3mndcdTAwMWOzbPC1RGEn18RcYuPcveFcdTAwMDO7Lr9cdTAwMWWYqsa2XG5cclxmT5vPUbcoLteO9jdu+8+tL6jGXFxyqr4lm6RcdTAwMTbJJb6EbLncz1x1MDAxZjSOsuKm55+zp3Ijac/Q1ODfXHUwMDA1gFJcdTAwMDNzXeFcdTAwMDFHeS6SUFNWSaJcdTAwMThWKv++RP1kostdpZnjOoI5l4RMMdFOcktKIYj3XHKqWiaILned5lx1MDAxZVx1MDAxNl+wJi+q9qNp2IxvXCI9XkbiPSih5PzG+aiSuz+4Pjab7evH/l0uuql27VO6y0g0XHQgLlx1MDAwZpHSOO20XHUwMDE53ylUu0FcdTAwMDabXHUwMDE0irijfOeOy1Pzf6SZXHUwMDAyRTVcdTAwMTFcYiOTrUh8fVIg7FdcdTAwMTWInGWPsJwpXHUwMDE3w0JH9ftrhXyhJ09+m0lcdTAwMWZcIrPVjE5cdTAwMWHP/LmxPin8bLZ411xin0bQNZhKNMCHRifqXHUwMDE1w6sunVx1MDAxOLzeudjt71ZpXHUwMDAwg3c0I6euhI1rnnnfw2ptdEpGXHK6MG+Ho1ZsXHUwMDFkpExDKdLbdbYr41+p1WlcXDeaxfB0xrBmmoaXXHUwMDBiPcU2qOS+W8BLW85cYju/cZhccqnfkFx1MDAxY/hD20BfL2CLL2n6a1x1MDAxN+9cdDQ4XZlAXHUwMDE5TuRcdTAwMWRUXGKo9y3zTE9cclxmyPyS12J+aH28WeSwXGKG06m05oRcdTAwMTNNU0DF9+Z6JW2O19KdXHUwMDEz6fRasatW7ESrjWal0bymg0Pj8b368tHbc/iYwawt93iUXHUwMDE5XHUwMDExeE6ON7zDs2bmqmOvui62X2ixXCJVXHUwMDA2xqvBqpB2r694s2Lfq83Kj1x1MDAwNzW7fjI+KFxiXHUwMDAwOPOH9+tChTg5Jlx1MDAwNVx1MDAwMWqSRDQuYz16nFx1MDAxOFJY7EZrrbu7RkSX/rDVaEbjl3hwLVd4XterxVx0i0FfKX5s3Fx1MDAwMLT5XHUwMDFkRz3E8Ldvwzky+OPt9//9c+qrM4lcdTAwMThcdTAwMWVcdTAwMWOdhO/wXHL/iP9cXNh4gUtcdTAwMGUvXHUwMDAzIHDr8fmXQGb7rVRcdTAwMWEvkIHTqIleONTxdkEvy1x1MDAxZoR8jlhcYkOSXHUwMDBm3fuWXHUwMDAwp/OawJMlcNxrg1x1MDAxNKeMRYeHxstj4CVqblLs6a7ETOjfSVx1MDAxNOBcdTAwMTGckv/PbJdcYpzzSqH0jlQ71zBPmFx0zktHunfgXHUwMDFj63X0sy3X6Lf4Slx1MDAwNiRcdTAwMTFH/JhA0ILmI0lcdTAwMTZJNyNoRXB0fpHN7Fr5p/M726q2y7mL7oHue5M5+6lcdTAwMDSCz9NFzlx1MDAwN3S9yW5rQ5JZiNGoKtc8XHUwMDA2mmtcdTAwMWZJ3Cul5fuiVtPth55LXHUwMDE3cY6tNe5cdTAwMDN3XGJ5g9DHXHUwMDE2uM/URVf3xO7v1irX61ekM7dOXHUwMDBmXHUwMDBmtsv1ZdBFL3czbbLoZVQ/RyykS1RFvFVcdD1wgW1cdTAwMDRnXHUwMDAzKo3EQmmidLyVNWe5XHUwMDFiK+W4aZBEPMgtae5cdTAwMDCo3ru10FTTXHUwMDAwXHUwMDAyXHUwMDAzR1x1MDAxZu1cZjE54vViiqUgXHUwMDE2LVx1MDAxNfFcdTAwMWbkZFx1MDAxN26mXHUwMDE4M1GvXHUwMDExXHUwMDE162y8TPgrM4vZXHUwMDFl5o00+EB6ayVaVlx1MDAxZtxMerpu4i5cXJrTZlx1MDAwNC/QaPW3V/1oVfQyKOlcdTAwMDLg5Tt2NFbxhkDThZPwgtcwXHUwMDE0Z0RcdTAwMTjESan2lWhNMoT5kZlE74LEJtl8QeLeaKR/6Vx1MDAxMi+y1/lst5VG62VV4EjuXGJtXHUwMDE53G4splx1MDAwM7xhhXVCXHUwMDE5ulx1MDAxNIR781x1MDAwYuo9wUBcdTAwMDBcdTAwMGVcdTAwMTnGjqZcdTAwMTi4aUtxXHUwMDAxWqFcdTAwMWTJf8WdKlWs28HfxotgXHUwMDAxPq27SH+4+YpcdTAwMTknIEZcbqRbJUsk7pA3YVC4gZ60WirihS/tY2bbr6+rjZKx9HJ4XHUwMDAyRlx1MDAwYlqRJHmkZLI8ctKRSXNufityWN987IeF3NPT85pRKzltmrX132lF/I+sSEZcdTAwMDWSjIOyoCW3x3Nj3cTNIIhH3Mehd2h9cvuNspdFWfyF8miwaY3/SH2UXHUwMDFlpUFaolx1MDAxNnTLnVZcdTAwMThetWq1bjVcdTAwMTVcdTAwMGIwU0b1c646nlA3XlxmpiRcdTAwMDd+cP5cdTAwMTjE7Fx1MDAwMuM0uuqMNVx1MDAwMShcdTAwMDOKU7SByMlo3lx1MDAwMHGVwcoteXNNdFx1MDAxNn7BXHUwMDFjXHUwMDAzXHSBRu5cdTAwMGVBJnTQNniKq0ZcdTAwMTVY3o/aWuFcdTAwMDSqyTZkknc8sTqt+/Ik+epxr/ZjLz67VUHci2vuZ+GkXHUwMDA1IG5LlyeuQ958uPLcXHUwMDFhRlhlaFwiqMn45lfy1MlIXHUwMDFhXHUwMDFjncDQXHUwMDA3+WlpXHUwMDEz96WhKcVcdTAwMGIvZn5cdTAwMGJyfH570Lx+OGvc9C5zRaNOK7BcdTAwMTOm201cdTAwMTOASDbyXlx1MDAwNIY3M7PjuaHEnoRcdTAwMTHoaXrK5Fr8OezHzFx1MDAwNqHxXHUwMDFjm1mu2nF/dv+RefRvMEp9s5X3XHUwMDExgadv//r26nGf0kBcdTAwMDFGxvNzzt+YRIptjVCkftxcdTAwMDK5XHUwMDE3M+91KqcuWadAO9I2VuFg88WxXHUwMDFkpYCzvnlek1x1MDAwNpSk9n7d7OV9V1DzNr/GgNZyil7XXCJAXHUwMDFhJVx1MDAxOIdMy/xErFx1MDAxMaxcdTAwMThs0IrLsZA521x1MDAxNXyLL2RK7zQ3iVx1MDAxN3TtRKy1/9DT61x1MDAwMIEri7iLqzJo/3Z+XHUwMDBiRlx1MDAxYmd32Vx1MDAxZFx1MDAxOZOTwJFNMN5yJFxiJ8dkyW1Izds6k85TXG7t12ZcdTAwMWaJXHUwMDEw5kdmXHUwMDAyvVx1MDAxZsQ+jEhkXHUwMDFm4Fx1MDAxOFxyxOrmt2HyMNNZXHUwMDBmj1Z8P8xv5lx1MDAxYnc7XHUwMDA1XHUwMDFhbLptmCWocU6LU85cdTAwMGZcIqsjJsyjXHUwMDBliPkpMnJcdTAwMWP8xuQ11HebMDv8zjNcdMhgf7RcdTAwMGZcXFx1MDAxMpnFP9JVa/0+/tFcdTAwMWb6+35cdTAwMWH4x8h4fo5/+GT+QVx1MDAxNlx1MDAxMblSaYGNgWff7DROXvBMP4yyXHUwMDFlNJlEM1x1MDAxYeAjO1x1MDAxYVxikq6G7Cm9YMZ+gVx1MDAxNVJcdTAwMWai9vNzV2FgvZCkhVxy+VDjp7RLXHUwMDAyQXZcdTAwMDbYdjPyXHUwMDFk+MlcdTAwMWNQXHUwMDBi7Fs/Mnv8d9KP2a7g2+hapvFecVx1MDAxMJT8vCZccoiT3t5cdTAwMDe8XHUwMDEzmFKgWFx1MDAxMf9kXG7o7CYhsTFx31x1MDAwMSmAXGI88JZxU5JSvzTXSMQrPzKTUP0grqFlYrqnIe1vUS3QlGhcdTAwMDM31/q9s4K7XHUwMDE0J7e3jbo5XHUwMDBi8+mmXHUwMDFhUkneYFx1MDAwNKznrfe40GqMakhcIuIgiYwr3u7116klOS1J3U4kdnI1Lk3JXHUwMDBm3GZ3XHUwMDE21UhXIeHHUY1v//k/zZfVhtmpVjj6Np/JPqZccvHnXGKJmpGSiZozXHUwMDEy9fx8ZDZcItI4w7mdkrDOW1x1MDAxMNKNt8VjOlx1MDAwMlx1MDAwNo0wZFx1MDAwN62G5LSF985vLXlfXHUwMDAxXHUwMDE2i5JcdTAwMDSci+/98jbdveNcblx1MDAwNuCW6YaTcyfriFx1MDAxNTegNMosR/LVbHcxRkfQoeSNXHUwMDFkXHUwMDA1arqAw0rqN/dvXHUwMDAzbiAtealA81x1MDAxMt9cdTAwMGZcdTAwMTK7XHUwMDEzk69mXHUwMDE2yo9cdTAwMGWK8/g0XHLMSt7tzU/GaEygXHUwMDA0d1xcRPTCO6e/NENJhjA/Mlx1MDAxM+j9IIZcdTAwMTIvb5hsjayscGaBvVlcdTAwMWJusy9z2bDQysls5XhbXHUwMDFjl1x1MDAwZXZSbcDAmcBcdTAwMTjeylx0rVx1MDAwNYk43mlcdTAwMWZcdTAwMDNniZooZbVRMzp7XHUwMDEykyvqd+zXZqdIqNi2uW+N59Cj1+LjXHUwMDE2Y341h4hZ/Vx1MDAxMVx1MDAwZTE0sn9ziNVWsVNZabenkoXY23xcdTAwMDZZeFx1MDAxYsvLpPrjddp+L7bbJ1x1MDAxMV2lN1x1MDAxM0fXv1F5/arDd/z+0Kg+rk5f0Oc1/T9eJyrPiCrfhn//9cdf/1x1MDAwNzDbVCwifQ== + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaXNcIkmS/d6/QlbzcZuccI97zMbWdKFcdTAwMTPd6GB3TMYpkFKAOCSktv7v645UXCI5XHUwMDEyoYvKYtVtqiqSI4h87v6eh4fHX38sLf3oPDbLP/619KPcK+bDWqmVf/jxJz9+X261a406XcL+v9uNbqvYf2a102m2//XPf97mWzflTjPMXHUwMDE3y8F9rd3Nh+1Ot1RrXHUwMDA0xcbtP2ud8m37v/n3Xv62/O9m47bUaVx1MDAwNYNcdTAwMGZJlUu1TqP1/FnlsHxbrnfa9O7/Q/9eWvqr/zsyula52MnXr8Jy/1x1MDAwNf1Lg1x1MDAwMVx1MDAxYWVHXHUwMDFm3WvU+4NcdTAwMDVcdTAwMTRotdBavj6j1l6jz+uUS3S5QmMuXHUwMDBmrvBDP453K1AvX/dcdTAwMGWLK6nWib/e1M3LjcHHVmpheNx5XGafp1wiX6x2W5FBtTutxk35rFbqVPnTR1x1MDAxZX99XbtBszB4VavRvarWy+320GtcdTAwMWHNfLHWeaTHpHh98HlcdTAwMTL+tTR4pEf/SjlcZjRcdTAwMDJ4kFx1MDAwZZXE14v8auVcXKC0tVJIROE1yJFhrTZCulx1MDAxMzSsf4j+z2BghXzx5opGVy9ccp6D6FxuZTN4zsPLlzUmXHUwMDEwylqtUGlnhFevz6iWa1fVXHUwMDBlPcVi4KQ2wlx1MDAxOIHCOTdcdTAwMThnu9y/XHUwMDFmXGJSKbruXHUwMDA3N5Q/vrlV6mPjP9FcdTAwMTmrl15mrN5ccsPBiPnCelx1MDAwNE+D13SbpfzzbVx1MDAwN2O1Rk9DoLl5vVx1MDAxZdbqN6NvXHUwMDE3Noo3XHUwMDAzpPRcdTAwMWb9+89cdTAwMGZAlO5KXHUwMDFjRK1cdTAwMDaNXHUwMDAy3OxcYl3LVm6ue8t7d1x1MDAxOXl2eFxcObnPZdeWXHUwMDEzjlBtXHUwMDAy0MpcdTAwMWGp0ErrnDfjXHUwMDE4ldJrXHRcdTAwMDRTacEkXHUwMDE2ozR85b0wesEgaoWLgyhISSZcdORDZsZo+WRzm1x1MDAwNl8olNTRQWbL7excdTAwMWPcqIRjNFx1MDAwNTJcdTAwMTDgnfZgwVx1MDAxOedgXHUwMDE4pNrIQFx1MDAxOSfoXHUwMDEyoFOj40pcdTAwMGVEQUvp6D9YOIjqOIhaa5SQxsLMXGI93y/s7VxcaFdoXHUwMDFjXHUwMDFj3ubP7zeJ37R/LUJBvOlGVWC1oWl3XHUwMDA2vFFgh1x1MDAwMGqkXHUwMDBmiFx1MDAwM3hPXHUwMDFlSmilMbFcYkWwVtFQXHUwMDE3XHUwMDBloVx1MDAxMEtFUSjiNqD97IH+NpXLy3RxOXucq96U16pcdTAwMDfrXHUwMDFiISTdiTpcYoiFKidccjqg71xmw1x1MDAxMFVcIiBcdTAwMGaqtFx1MDAwMkVE1erEQlx1MDAxNCyRXHUwMDE0TTFg0bio9bFcXFx1MDAxNFx1MDAxY01cdTAwMTdFNz27XHUwMDFi9TZvdNi4vu5cdTAwMTazcFx1MDAxOXZLjX27l3CMXHUwMDAy6sBcdTAwMWKhKcxcdTAwMTNcdTAwMDR8RFx1MDAxMD2HeU1cXNRcYutQ9mNKciHafy25lPm5UT9cdTAwMWa5JHxcdTAwMWNEtfDOXHUwMDAy+plcdTAwMDHaetquLtv1PVxym5uynFp7Slx1MDAxZlRPk1x1MDAxZefpzlKgJ1x1MDAwNmdR05/aXHJLesJtgI6UvDGoPGKSqahw4KxcdTAwMTBcdTAwMGJcdTAwMDdRXHUwMDE5n3RykmKbfIdcdTAwMTNdllx1MDAwNd+53Xpyjerm8Xr2XGbyp/WkK/pcdTAwMTRgIDmvRLLeoSBHaUYwqomMXG5E68lFXHUwMDE5n9ysXHUwMDEzgDWk6oWaX6SfXHUwMDBmRq0wcVx1MDAxOPXgPOiogHhcdTAwMGKjK141fKdzs1Jo3+CtKUH7ot5MOEbBXHUwMDEzXHUwMDA2lVx1MDAwNVx1MDAwMUpcdTAwMDLFe1x1MDAxY470ylx1MDAxOYKGoD+EQYr3oJKLUUnwcdZEXHUwMDAy42JglD4pXHUwMDBlo8zMlNJmdozuhL3qWlXmMlx1MDAxZFFcdTAwMGXXzcNj2OydJFx1MDAxY6NSyoCYptBcdTAwMTTKhTM4mrwniFxuY+mOWCFRJDgxSjZGMUBoOb/M6JzcqME4iFwiKE30Rjo9M0b1zXpYKFXu08f3WpRy5+FlfT1MOEbRqEA68j9cdTAwMWWU0TKShntcdTAwMTZMXHUwMDEwOMKuQk5MYYKdKPlcdTAwMTJA4qNcdTAwMGKHUFx1MDAxZJu7d2hQ0q2ZPeu0eqMuOnBdusruXHUwMDE1W6dcdTAwMGYtma3m80lcdTAwMTdM7EWV8kQzXHRcdTAwMDFcdTAwMDCRtYyfWSdA4ywvjoIzXHSGqJVcdTAwMTL418JBVMk4iKKj2dL+XHUwMDFkq0v6+mJze9dm95dVuYHb+6enzZxNOkTBm4Cw4STxXHUwMDFhXHUwMDAxUo5hXHUwMDE0XHUwMDAzLSjWkGJcImbuklx1MDAwYlGyMbpjOMdF+jlRUVx1MDAxZFx1MDAwYlFtke5cdTAwMWH42SU9bi7bo3C7lrtfbV5f7eNtRVxc51x1MDAxMlx1MDAxZeaVkIEha1x1MDAxNN4rXHUwMDBmVo2GeVwiXHUwMDAxSMhcdTAwMTDSeUy0oDfaWWW1XTQn6lxcbF6UvIvxRkaXSN+C6F7n8r6mbnqnzZW73HLnMXN8VF3/XHUwMDFkIGqEJS6qnZMjqXvOOVx1MDAxMVd1gqhcdTAwMDCr/eSKJS6FsVx1MDAxNtTCIVTGMlFcdTAwMDM0a1rI2X3oxe7DRqHaq4Rdd7ZV92erjfr+RdLDPNHMwFiHJJPAOI3KjkDUXHUwMDA2XHUwMDAyXHUwMDFk31x1MDAxOGe4jiixXHUwMDEwReEleVHQi5ZysjLWiyryoUJqNbtcdTAwMTNcci+ecvumqFx1MDAwZm/NQ72zv3N73To/TLhcdTAwMTNNOZJD9D3BSclcdJtcdTAwMTGEelx1MDAxMVgwSoNcdTAwMDaLTiVXLCktkWJeRFksXHUwMDA2QJ1cdTAwMTdxXHUwMDAwXHUwMDA1Vlx1MDAxMHTv9OxcYr16KKZPa21wl9nH7VpVmePc+i9Ois5Q6GRJXGZJ+qaEQZr3KD6eMVxugdJkrFZcdTAwMDOp/YiLSlx1MDAxYUbpXCLxXHUwMDE05Vx1MDAxNm3908ZjlL+wgWhZxZvVou2KKp6c7Oid5mrT5DLd6nphM+lOXHUwMDE0REA6wztcdTAwMDOoud4uUpPAb+BcYj2kokjqk2Am8CS3jMRcdTAwMTlnhV84JlxusVFcdTAwMWWFXHUwMDE2Xoj3rCx5XHUwMDExVjvdwoYt75ZqXCJzXuymdlx1MDAxYlx0Ryj5UGk8ek1cXFR7gcNy3itcdTAwMTF4hSRDXHUwMDE0XG6Z4Fx1MDAxMlx1MDAxMpLxxEXkwiXtnYivXHUwMDE1JXySdphcdTAwMWSehUZcdTAwMTWuWts7t08rXHUwMDA3t1f3ze398/VMwuGZsi6w1pIs1I6IJrnRXHUwMDExfMpAopTOoqbJwORWiqIgLi1cdTAwMTDEwvFQXHUwMDFiW2+PLJSMtbPT0OPMRerk5nxv67540d3aL4dcdTAwMGZcdTAwMGb791x0hyigXHJcdTAwMTR7SFCS5LBcdTAwMWGN8Fx1MDAwNGAgnU86RHlcdTAwMGLJLbe3aLVfQFx1MDAxN+ohNtukpLXo4Vx1MDAxZEp+fcs9XFyd7tQz9+XD7PLKSemyW096OpRcdTAwMDHKy1x1MDAxMp5cdTAwMWOQ5FxyIUNcdTAwMDAl7qlcdTAwMDMhNTlZrZXxNsGLSob/035cdTAwMDG1fHwps1x1MDAwNE9cIvZcdTAwMWRL8/dcdTAwMWJcdTAwMGad86Otq8xarry6fX2xXazmz1x1MDAxM1x1MDAwZdFcdTAwMTS5UMF1MsJpY6xcdTAwMWYpZSaM2sAp+lx1MDAxMUT0SEol14tcdTAwMDJdsDSChcuHmtgw7zQ4XHUwMDE33Y79pk7avYHbTmpzt3l3XHUwMDEwbuyk6j19eZD0ZFx1MDAxM3jL2+eFQCSkRpcwnnNNXHUwMDE4KND9XHJ3hGKdXFylXHUwMDA0QCYk1OIlRH188VxiOO2dXHUwMDE3XHUwMDA2Zlx1MDAwZvS5Wlx1MDAxNcqNVLpWz2RrOrO50fBcdTAwMGa/WMvPUuBkXHUwMDAyQqDXllx1MDAxM0p+dFWJMCqJXHIxepBcdTAwMTdtXHUwMDEyi1FjUJOkc4vmRJ2MdaIgOIXt5DtcbpzOT1x1MDAwZotccn9YujqrXHUwMDFkZU+0bj21Vs5cdTAwMTJcdTAwMWXo0fDKJil2pYiWXHUwMDEy3Vx1MDAxY07ZO7qsSMqT4JdcdTAwMWGT7EXRWuJcIkYvmlpyJjbhpFhcdTAwMWPCe7YsZc+3m1x1MDAxYrWtjaNMa2tXn3SOz07bSW+TI6VcdTAwMGVcdTAwMWNcYqKhxEhp1ofVkldcdTAwMTA4a/t1I+RpXHUwMDEznFx1MDAxMVx1MDAwNcnKwZk59iCZV1x1MDAwNV5spT2QNDBWKJw9zvuV8/rpffWyU8BcdTAwMTW3WXkqrJVcdTAwMWbuXHUwMDEyXHUwMDBlUfAuIDFvhffaaFx1MDAwMGlGMKpcdTAwMDKKn2iIqYJEmVxcQe/J0Kywi0dFMX7fp1x1MDAxMZr3jNvZNydcdTAwMWbs7dhuSp7ByeN1/cGmsVPYLCdcdTAwMWOiSlx1MDAxMDxIXHTT/UUhhVx1MDAxZNlcckJyMTCaVD1xVMNro8mFqDZcbr3Ri1bH7OPT9oCO63tRzVx1MDAwZdH1Qlhv3e1cdTAwMTfSXHUwMDBltleObjZuXHUwMDFl1sKjxEOUXHUwMDAzPVFRYclcdTAwMGaBXHUwMDE4dqL9tKi3gvyr8kbK5Fx1MDAxNo9cdTAwMDCS/Vxit4iCPt6LUtBcdTAwMTDc9WB2Lrpmd+5ztfW1p/AqbNbq4DauNm5cdTAwMTNcdTAwMGVR8p+BIMmOdPspkFx1MDAwYmdHMcqro16C1nR7olQvcSBVvDuVSfOCgVSa+I54zit05Edmz91X7/Nb6V1cdTAwMTf2Llxua2m7nXdHO+W4XHUwMDFhp1x1MDAxMbBcckN0tFx1MDAxZfP1VaV8u1p+XHUwMDA3Ru0sbUVZXHRpZTn1NJq6573JXHUwMDA2eEcl31x1MDAxOTDuUyVOnVa+3m7mW4SBcZxqrVx1MDAwMmOs05ZmXurIkskrTlx0oUG/o5TWRJ9hUs8xxWu5qL5cdTAwMGWnL1x1MDAxN1x1MDAwNsCK3O+Gvdjcv8Dl5ulx+7RSa/m6yVxmmndccqEw32o1XHUwMDFlfrxe+fvPae+bOd+9XHUwMDE2+TOzVz0uioNmoVx1MDAxM+7ordne9+VvybAucDZ+M4vgLXJcdTAwMTbf0c5v+bKS71xcXHUwMDFk7Fx1MDAxZjdSVVkopo+PN1RcXD5iqnmNrj99azM/bblcdTAwMTZNXHUwMDExTyalN8yjjXRcdTAwMDFcdTAwMDV+0CwriG/Hl2HTs/LqjVx1MDAwMFCpVMatSipHI1BI4ZY+30VcdTAwMTbTX62KXCJT4HkvXHUwMDAzyU2DUX3+umHVaMlcdTAwMWJWv45FT7Wq89LpKt5V7vaq+96Yxoo5yJz8juhcdTAwMTdcdTAwMTCf6rA0o85F6/LfQr8tbu7Ui4VO53Hn3pnlp9Ku6vaSvqShdSB5r1x1MDAwNVx1MDAxMoSUVsRyhlxyQEEgyd+DI1x1MDAwNjy0pTtpXGbIglRCLdy6MFxijMcoXHRJ40HNzn+2lrP3XHUwMDBm4U1W5S5219IrWbVij4pJhyh4XHUwMDE1oNFWcl9cdTAwMGIyWTWGUFx1MDAwMVx1MDAwNlx1MDAwNXlp3tGdZI7OXHUwMDA1XHUwMDE4Xi9cdTAwMWNH91x1MDAxMYowuqbRryrRZvZUhzvdyJz27uzmyurxzr06XHUwMDBm5ermL27DNsvCMFx1MDAwNqSYvSdcdTAwMDLurbejPYFcdTAwMTVcdTAwMDaSN1x1MDAxYzpP0TvJzVm80t5TXHUwMDE0WDg3qlxcrFx1MDAxYjVWSVx1MDAxMlc4e6TvPS7fhPlwfTt31MvlTn0h3HWtxPNcXCG04jVGr7mkeyTOXHUwMDEz0SWKS1x1MDAxNJIgqpz93I7YXHUwMDE4omtcdTAwMDOtaFx1MDAxMJKbwGk1QT7CM9tGYl7cKFx1MDAwNqL7dV52XHUwMDFiXG6Dyir4ukW3aURcdTAwMTex97jTbsPxg6rD9uPBtj+utWcjulPl429EoMthWGu2J9tcdTAwMTTxxjibQsKIXHUwMDA2Zdzs4nE1tfm0dbyXlb1cdTAwMDPZvs4101x1MDAwZvthXFxcdTAwMDJxqlHNz+1cdTAwMWKiXHUwMDFlfI5cdTAwMDa3XHUwMDFmXHUwMDEz0c6Fz05fXHUwMDEzMUHJus1+cot5Ja9R47hNsXS0xtHryb+p6IlcdTAwMWSvNuUoKqDtb9Hs96yXoyZcdTAwMDVcdTAwMWVcdTAwMTVyt/D5iMdP2FSSsK+mtPXkXHLsxsvZXHUwMDE3yVx1MDAxZkXtonq/bVx1MDAwMVx1MDAxZXZ6XHUwMDE1n013u+eYbOhbctVAmDFeSEUqeVQ2qn5iXVvlUVxu87l64kq+QN7ke9BvKfJcdTAwMTn9lfnIZMBTx+f1nOZ2rO9pUrPSOzg9Or9q1zOt+onRqdst7dLJxqc3XHUwMDAxeUyJwnpBXHUwMDE4XHUwMDFjQadcZlx1MDAxY0hcdTAwMDOWa+GI9Hxcbp2AXHUwMDA15ybw8a9AJyrlXHUwMDAwo0dcdTAwMDH8Nuh8K/NcdTAwMTa79qiNJOeJcnbJeLeVrm0yc7BcdTAwMTdP9e4y3jzI9C9u8zVcdTAwMDMhN4RBL/uiUWvrRurd+SwuuiOSdJxcIsKb3I3rXHUwMDAwaHjBZ+G6KHlcdTAwMTWfeFPsRH20XHUwMDEx9FtcdTAwMThN27ua9duVxure6nlnuXZb3ln7xZ27Z0trXHUwMDEwj9GOlCNnf4drNblHXHLvXHUwMDE1XCKhJrj1QoJrNWl00jrxO0b56aduxm9sXHUwMDAzT1x1MDAxOFx1MDAxNVx1MDAxNFneUU58mOnKXZu9W1x1MDAxM+srOzv32Vx1MDAxNYzd2fblYV58XGKh2lx1MDAwNki+UlxuirROR49cdTAwMTjtl8FpRVxi9U5JQb/d51LDxXJJlfLjXHUwMDAwlcR1pVx1MDAxNUSB0ViylkhkXHUwMDFiJDYwMF4765ltSHKYalx1MDAxNKLIXUeV/Uo3+nJholxuM2m9d5KRT1ukXdcvU3p143LvcDZcdTAwMTX257T33czVTHhSf+q08+Ji9XBv/ab31PgqdUdwILo+XHUwMDFm51x1MDAxZp/aIOZsXHUwMDE0yvdcdTAwMWPEuFJO5eHo2nWaZ49yZb1X2c2sJZ6gXHUwMDEwOVx0LFx1MDAxYc3HL1x07q0w3v1JXGLD59xcdTAwMTlF/jW5PfRcXL9kxSyc85+6Ic9xu1x1MDAwZfOOKuiLsL0s072101x1MDAwYpHN71x1MDAxYtvJp5dlsp0/qdxcdTAwMDClXHUwMDA04tDcjlSPIJS9v6BcdTAwMTBoiK55Jz/ZcFxcXHUwMDE1TbkyIVx0XHUwMDAx3lx1MDAwN9JbXHUwMDAwj9yHSkyo3yDvT+BFQzpUOOWjNVx1MDAwNT8hXG5cXH6iXHUwMDE27oCmaD/xXHUwMDExiEq2SVx1MDAxZF1cZn1cdTAwMTOhd729w4vl/HZnZ2VHnl2J1XVI/uI16lx1MDAwMKxcInhwP3mMXHUwMDFlY/HcidRcdTAwMDSSXHUwMDBiMFx1MDAxNOk8XHUwMDFmLeBJmlx1MDAxN1x1MDAwNU6kWLt4zSHIaOMwalx1MDAxNUU4mozZI31ntbZXrbWrdrmi0o3Udc2ellx1MDAwYknHKFx1MDAxYVx1MDAxOWiB5JpcZoAzY/2cTUBcdTAwMTSagihywjfBzXKl5lx1MDAxM7VcZs4tXHUwMDEzXHUwMDExab/4rdkygFg2yrkh3nw/O0Srayd+67GweplXXHUwMDA3T83zfPmweFx1MDAxOXcqeEJcdTAwMDK9dC6g6GmRmDc5Slx1MDAxY85DXHUwMDE4TVx1MDAxMVihUJI7zmvxPTJcdTAwMGZkoIhE8mJcdTAwMDFZgvGRj1x1MDAxOVx1MDAwNHqv+Cwzy1x1MDAwZV8hUZLxxTbB8mHo1d8p875Vjs3TXHUwMDAyplfBWURcdTAwMDPvqDG6gHSx2Mvu11x1MDAwZdPiuuuf0ie4XHUwMDFld1x1MDAxY2lC1jPYXHUwMDA0LHeC9rxcdTAwMWRFXHUwMDFhXHUwMDFjWXAjscY9KZSxRDC9/tySRizXhcCQtCBkU5RcdTAwMTAmmltcdTAwMWGsaSA3vFx1MDAxNMJb0d8zM8Z1uaU198iYz3Lz91pcdTAwMDBxh1xiw/2kXHUwMDA1dMq9zkTw2/hcXFx1MDAwNDvDfmO5mbF/WMrc7V9cdTAwMWTpjebVQ+8207kut+1jsrfAXHUwMDEw5lx1MDAwM97agqiJXHUwMDE1Kj16yKly/eI7XHUwMDE0ktgpfvKs6Im1S6gn8JHoyVx1MDAxNi+bWYWWnjuKzVx1MDAwNdrftbnlNH1oiqliPsy1ZK+3msvmunj8y4LGXHUwMDAwmY1657j21K8uckOPpvO3tfBxXGJdfVuiXHUwMDAx3tdanW4+vGzTXHUwMDBig5c7XHUwMDE3uf3tMlxyoP+Oeuily2Htik3vR1iuXGbbZKdGXHUwMDEz83q504is4Vx1MDAxNGkoeXq71lZp9Cs1WrWrWj1cdTAwMWaeTFx1MDAxOdZU3/A80Vx1MDAxM5yDjO9cdTAwMWLG68rkqmdcdTAwMTfY01x1MDAwMfVcdTAwMGLKXHUwMDFh3/RcZlrYgMNcdJLxK1x1MDAxN21o1H+51IHU5Fx1MDAxYVR/Z4P83Fx1MDAwMtXkosZAcDrfSevpfy0mXHUwMDE0NVwi14EpxedcdTAwMWIrjUpGT1x1MDAxNXshhY6rXHUwMDAwXFy0XHUwMDE3cZKES2TW8q3OSq1eqtWv6OLAdfwoP3/01lxmXHUwMDExpm+zxS6PMiVcdTAwMDLPRf2aj6ZWpP5cdTAwMDbHXFzwXHUwMDE05JvPtFuS6lx1MDAwM+2lN8R7lHt5xqtcdTAwMGb7Ua6X3lx1MDAxZdT0nZ/RQUFAZkM3RIO1Rlx1MDAxYTM+Jkk0qK+6pNbWXHUwMDFib8aGXHUwMDE05tud1cbtba1DU3/QqNU7o1Pcn8tltupqOT/mL+grRa+Nmn+T33E4Plxm/rY0sJH+P17//p8/Jz47XHUwMDE1i+H+1XH4XHUwMDBl3vCP6J/vdl3g4lx1MDAwZsxcdTAwMTQ0rzp6XHUwMDE4+Fu+a3rQSqTvXHUwMDAyXGacMoq4hTPK4OjiOlx1MDAwMZ9cdTAwMTNcIkKTojTuk1x1MDAxNXRcdTAwMTNJTUBKQTjuXHUwMDEyQoJcdTAwMTYnkXn0JvBoXHUwMDE0N1j2Q2vJr9VcdTAwMWbgSVx1MDAxMkj8f+a6ROCclyTDvFx1MDAwM7qFkSbdr16CXHUwMDBi6lxy3TuCOadcdTAwMDOMn+64hr/F7+Q/YnHEP2NcYnqn94hcdTAwMTNFXHUwMDE4aVx1MDAxYzbqPKRA8lTvKM5pZFx1MDAxZs9ubaPcLGbO2/uq53Xq9EOFXHUwMDBm89NEzlx1MDAwNzTd5LWVdlx1MDAwZYRcdTAwMTjO2fJOzUDxjk3pnJRcbj/XXmWy+1AzaVwiXHUwMDEwXHUwMDEyrHZfeLTJK4K+dmP+VE10eUfM/na1dLV2SVx1MDAxYXPz5GB/q1hdXHUwMDA0TfR8N5MmiZ5H9TFagT5eXHUwMDExkSP22pnZN8xOx1NcdTAwMTJphVTE5/hcdTAwMDRuzV1cdTAwMDEs4qhnQKJcdTAwMWRcdTAwMTSUXHUwMDE09y6Unz1cdTAwMTJpomdcdTAwMDBhXHUwMDAyR1x1MDAxZu1IgFx1MDAxMmsk/TXuKIhCoyT2Yzw3K3U+kjP5mUyxzlxuObfVxm/lXHUwMDE101x1MDAwM8wrZfBcdTAwMDF6a9FYllx1MDAxZdxcdTAwMDd7smhcdTAwMTKcXHUwMDA146JcdTAwMWPBZbdK/oypXy2Jnlx1MDAwN4UuXHUwMDAwzjtznLGSXHUwMDBmMpqsmoRcdTAwMTe8QEKciIZlzLhO+51ITTyE+Sc1jt530pp474XxR7p5x4fcvKPubHrUSqL3sjJwJHZI+TG43UhCXHUwMDA3+KhccuuE1EZ5bsvyXHLbVEFDXHUwMDAwXHUwMDE0XCJcYsaOTFxm3KR1voC7oDrSqJJbbMrIdtSfzotgQaFmfodf/1r3XHUwMDE1cU5AhFx1MDAxNEi1XCJcdTAwMGIk7uw35lC48Vx1MDAxZlqFkmjhc9Ob6f7r91VG8Vh6vjxcdTAwMDajd3qROHFkpm3MRXDeR1Olb/ZHrW489MJcXObx8WlVy+WM0vXK2q/0XCL+LS+S0nyomXL9XHUwMDAzd4zGkcMktFS8nmRRXHUwMDBiukNexi+WXHUwMDE2PeYx/43iqH/ajv9KdTStfppl+mBcZt+tM0hJVIJ2sdVcYsPLRqXSLidi6WXCqD5cdTAwMTapdXzrNG1cdTAwMDRZdZS8vmVj03dFJzFSp6znXHUwMDEzqsnXW66vJa8/ZGOqf4C1JetcdTAwMTJGO4VTemd+2MhcdTAwMDBVYJXnXHUwMDE0XHUwMDFjSC8hsmFrXHUwMDEwqo1cZri3XHUwMDA3elSWrHIsg8l1Q4pcdTAwMGL95tLyXHUwMDA0rDPqK0L1aFB7O4hPb1oywuWl53RcdTAwMWVqcpXCXHJK/pZcdTAwMDbJTU/S0mrF7W608vL3ZvuxQOpfXHUwMDFkhdBcdTAwMTdF6eh+37H6fetcdTAwMWRzg9mj9NHZzX796v60dt29yOS1PCnBdpjsKI3OkGrkQ1x1MDAxNLRA6e1o3SmRJ6GF8WSeXHUwMDE433huXHUwMDA29zG1rSmYXHRcdTAwMWK2JoRqkoOMdfi6XHUwMDFh/VdcdTAwMWP9ilx1MDAxNjFzTDg+Lv176SXiPiaBXHUwMDAyXGaN52PB30yR6aRrjFx1MDAxNPJcdTAwMWR1XHUwMDE3U+91XCJNXHUwMDE3hFxulCNpY6Xpn1x1MDAxYTmyOYyrZT2FXGLe4m7UlMqLT1uv8oHzvHvGcz9cdTAwMTIhJ3BcdTAwMDBSYVx1MDAwZcBcdHR82ICa0Fx1MDAwMseK/smyZi6rmGA1XHUwMDA1ky/gXHUwMDAwcZF+eihYiq5iUpwnXHTCfcOF9laPXHUwMDA3elBcdTAwMDFdI3fBrWelNm682OFr2Vx1MDAwN9FGQYzCsjDiqr5J3IN71HE9oHRa0vB/b+5cdTAwMTFcdTAwMGJg/kmNYfeLyIeRscVcdTAwMTc0XHUwMDBloKDrZ+9cdTAwMGaDXHUwMDA3qdZaeLjse2F2I1u73c6Bc8l2YOSZXHUwMDAydl7kw4zhxZAh/+UlXHUwMDA2SE5cXDllLZeifJ//slx1MDAxM1x1MDAxY9Yk9tE/1O1cdTAwMGKXQ6aRj0/sXHUwMDBlf5t8XHUwMDEwnfaRpv7fTT56g2DfS1x1MDAwMvlcdTAwMThcdTAwMWHPx8hcdTAwMDeXp8VcdTAwMTkvzbAw7zr+aPrNTqTxXHUwMDAy9yVBpygmeTJgXHUwMDFjyT2QTlx1MDAwYlx1MDAxMNGT/Vx1MDAxYe99fO1DibSHqHzCeJ1cdTAwMGZQsCZUxnKX7HFTNuTdjdJeKvLfWsFY41x1MDAwN7DAgfUri8a/xfhm5Fx1MDAxZdNDQSSGXHUwMDBiJTiuXHRyr9xBYFx1MDAwMlx1MDAxM+CVTor0XHUwMDFj4ZWy3HOByPHH2Mf0ziZcdTAwMDOyM2FcdTAwMTTc5Vx1MDAwMFx1MDAwNaBzgLxs/nuTjTjA8k9qXGarX8Q1tI7lXHUwMDFhUvSLe9/RwnzdbKz2uqc5dyGOb25qVX1cdTAwMWFmk801NK9cdTAwMDKR7XnBp11ZO9xcdTAwMWKaZjvgynVNXCKK7suUXHUwMDBl+5+lXHUwMDFhOKk63Y4lRHmbL1x1MDAxOdxcdTAwMTdcdTAwMWVcdTAwMGU8jWp8b8NcdTAwMThUVnxIaX2aaiz91//Wn5dcdTAwMWGmV1mZ4beZJ/uYNMRcdTAwMGZcdTAwMTJcdTAwMTLw8VvUKOKAtO85pWA6Jlx1MDAxMmni3lx1MDAwN55z4ZJcdTAwMWKB2JGaK1wiXCKBRd7VzkdcdTAwMTR4XHUwMDFiT0g+rSbIv5NIXHUwMDA3329RJGDSKqTl4lx1MDAxNW208dKSjejx7Shk/PRiO5+OfsinWOiP2OiMjGR6wFiKZlx1MDAxZZDPylx1MDAwND6uwFC8x/GdXHUwMDFmKuBSW3IqgMhV31x1MDAxZt2MMnVcdTAwMGL+0JCI8lx1MDAxMNEl5Vx1MDAwZp6X7u3YkFxcoPlwXGbNjVx1MDAwZpif/N51V6l4XHUwMDA09y+PgfeLKFxuQPwxcyRcInjh/Fx1MDAxZOdg1dxGXHUwMDBmM+kw18hgunS0JY5cbvvbiXZg4HSgNVx1MDAxZj/FvJDsYPR4XHUwMDAwXHUwMDEzOEt6SkpyXHUwMDBicko70lx1MDAxOc7BmurB7IQ2uZFcXPvPTlx1MDAxZlxc1+Hn2C/p06slkfa/QyxiXHUwMDEwKX6yiJVGvlVabjYn0oXI28yDLryO5dmo/nix21x1MDAxZvlm87hDs/Tq42j+a6WXrzp4x1x1MDAxZve18sPK5OV8XtH/48VQ2VwiynxcdTAwMWL++vuPv/9cdTAwMGZcYuGAVyJ9 - virtual_size.heightvirtual_size.widthself.scroll_offsety = scroll_yx = scroll_xx = scroll_x +self.size.widthBoardApp + virtual_size.heightvirtual_size.widthself.scroll_offsety = scroll_yx = scroll_xx = scroll_x +self.size.widthBoardApp From 738837fd663cbd03b375568e50458b310e32d274 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Feb 2023 12:06:48 +0100 Subject: [PATCH 15/17] review update --- docs/guide/widgets.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 947bda5df..58a76d7aa 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -201,8 +201,8 @@ TODO: Explanation of compound widgets ## Line API A downside of widgets that return Rich renderables is that Textual will redraw the entire widget when its state is updated or it changes size. -If a widget is large enough to require scrolling or updates frequently then this redrawing can make your app feel less responsive. -Textual offers an alternative API which reduces the amount of work required refresh a widget, and makes it possible to update portions of a widget (as small as a single character) without a full redraw. This is known as the *line API*. +If a widget is large enough to require scrolling, or updates frequently, then this redrawing can make your app feel less responsive. +Textual offers an alternative API which reduces the amount of work required to refresh a widget, and makes it possible to update portions of a widget (as small as a single character) without a full redraw. This is known as the *line API*. !!! note @@ -211,7 +211,7 @@ Textual offers an alternative API which reduces the amount of work required refr ### Render Line method To build a widget with the line API, implement a `render_line` method rather than a `render` method. The `render_line` method takes a single integer argument `y` which is an offset from the top of the widget, and should return a [Strip][textual.strip.Strip] object containing that line's content. -Textual will call this method as required to to get content for every row of characters in the widget. +Textual will call this method as required to get content for every row of characters in the widget.
--8<-- "docs/images/render_line.excalidraw.svg" @@ -239,7 +239,7 @@ You may have noticed that the checkerboard widget makes use of some objects we h A [Segment](https://rich.readthedocs.io/en/latest/protocol.html#low-level-render) is a class borrowed from the [Rich](https://github.com/Textualize/rich) project. It is small object (actually a named tuple) which bundles a string to be displayed and a [Style](https://rich.readthedocs.io/en/latest/style.html) which tells Textual how the text should look (color, bold, italic etc). -Lets look at a simple segment which would produce the text "Hello, World!" in bold. +Let's look at a simple segment which would produce the text "Hello, World!" in bold. ```python greeting = Segment("Hello, World!", Style(bold=True)) @@ -257,7 +257,7 @@ Both Rich and Textual work with segments to generate content. A Textual app is t A [Strip][textual.strip.Strip] is a container for a number of segments covering a single *line* (or row) in the Widget. A Strip will contain at least one segment, but often many more. -A `Strip` is constructed from a list of Segment objects. Here's now you might construct a strip that displays the text "Hello, World!", but with the second word in bold: +A `Strip` is constructed from a list of `Segment` objects. Here's now you might construct a strip that displays the text "Hello, World!", but with the second word in bold: ```python segments = [ @@ -268,7 +268,7 @@ segments = [ strip = Strip(segments) ``` -The first and third Segment omit a style, which results in the widgets default style being used. The second segment has a style object which applies bold to the text "World". If this were part of a widget it would produce the text: Hello, **World**! +The first and third `Segment` omit a style, which results in the widgets default style being used. The second segment has a style object which applies bold to the text "World". If this were part of a widget it would produce the text: Hello, **World**! The `Strip` constructor has an optional second parameter, which should be the *cell length* of the strip. The strip above has a length of 13, so we could have constructed it like this: @@ -276,7 +276,7 @@ The `Strip` constructor has an optional second parameter, which should be the *c strip = Strip(segments, 13) ``` -Note that the cell length parameter is _not_ the total number of characters in the string. It is the number of terminal "cells". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to leave the length parameter blank so that Textual calculates it automatically. +Note that the cell length parameter is _not_ the total number of characters in the string. It is the number of terminal "cells". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to omit the length parameter so that Textual calculates it automatically. ### Component classes @@ -310,10 +310,10 @@ The `render_line` method calls [get_component_rich_style][textual.widget.Widget. A Line API widget can be made to scroll by extending the [ScrollView][textual.scroll_view.ScrollView] class (rather than `Widget`). The `ScrollView` class will do most of the work, but we will need to manage the following details: -1. The ScrollView class requires a *virtual size*, which is the size of the scrollable content and should be set via the `virtual_size` property. If this is larger than the widget then Textual will add scrollbars. -2. We need to update the `render_line` method to generate strips for the visible area of the widget, taking in to account the current position of the scrollbars. +1. The `ScrollView` class requires a *virtual size*, which is the size of the scrollable content and should be set via the `virtual_size` property. If this is larger than the widget then Textual will add scrollbars. +2. We need to update the `render_line` method to generate strips for the visible area of the widget, taking into account the current position of the scrollbars. -Lets add scrolling to our checkerboard example. A standard 8 x 8 board isn't sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares. +Let's add scrolling to our checkerboard example. A standard 8 x 8 board isn't sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares. === "checker03.py" @@ -362,7 +362,7 @@ Here's the code: We've added a style to the checkerboard which is the color of the highlighted square, with a default of "darkred". We will need this when we come to render the highlighted square. -We've also added a reactive variable called `cursor_square` which will hold the coordinate of the square underneath the mouse. Note that we have used [var][textual.reactive.var] which gives as reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor. +We've also added a [reactive variable](./reactivity.md) called `cursor_square` which will hold the coordinate of the square underneath the mouse. Note that we have used [var][textual.reactive.var] which gives as reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor. The `on_mouse_move` handler takes the mouse coordinates from the [MouseMove][textual.events.MouseMove] object and calculates the coordinate of the square underneath the mouse. There's a little math here, so let's break it down. From b12c5e1cdfe4ae1f1c0e73d29c6f5a5c7cdb18da Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Feb 2023 12:12:27 +0100 Subject: [PATCH 16/17] typo --- docs/guide/widgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 58a76d7aa..ee54f5899 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -362,7 +362,7 @@ Here's the code: We've added a style to the checkerboard which is the color of the highlighted square, with a default of "darkred". We will need this when we come to render the highlighted square. -We've also added a [reactive variable](./reactivity.md) called `cursor_square` which will hold the coordinate of the square underneath the mouse. Note that we have used [var][textual.reactive.var] which gives as reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor. +We've also added a [reactive variable](./reactivity.md) called `cursor_square` which will hold the coordinate of the square underneath the mouse. Note that we have used [var][textual.reactive.var] which gives us reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor. The `on_mouse_move` handler takes the mouse coordinates from the [MouseMove][textual.events.MouseMove] object and calculates the coordinate of the square underneath the mouse. There's a little math here, so let's break it down. From f8577f79d7a826bf422127d817bb5244c03b6b7f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 6 Feb 2023 11:34:37 +0000 Subject: [PATCH 17/17] review fixes --- docs/examples/guide/widgets/checker02.py | 2 -- docs/guide/widgets.md | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/examples/guide/widgets/checker02.py b/docs/examples/guide/widgets/checker02.py index f108de8c5..b498ef204 100644 --- a/docs/examples/guide/widgets/checker02.py +++ b/docs/examples/guide/widgets/checker02.py @@ -1,8 +1,6 @@ from rich.segment import Segment -from rich.style import Style from textual.app import App, ComposeResult -from textual.geometry import Size from textual.strip import Strip from textual.widget import Widget diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index ee54f5899..8c8f20c3b 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -268,7 +268,7 @@ segments = [ strip = Strip(segments) ``` -The first and third `Segment` omit a style, which results in the widgets default style being used. The second segment has a style object which applies bold to the text "World". If this were part of a widget it would produce the text: Hello, **World**! +The first and third `Segment` omit a style, which results in the widget's default style being used. The second segment has a style object which applies bold to the text "World". If this were part of a widget it would produce the text: Hello, **World**! The `Strip` constructor has an optional second parameter, which should be the *cell length* of the strip. The strip above has a length of 13, so we could have constructed it like this: @@ -280,7 +280,7 @@ Note that the cell length parameter is _not_ the total number of characters in t ### Component classes -When applying styles to widgets we can use CSS to select the child widgets. Widgets rendered with the line API don't have children per-se, but we can still use CSS to apply styles to parts of our widget by defining *component classes*. Component classes are associated with a widget by defining a `COMPONENT_CLASSES` class variable which should be a set of strings containing CSS class names. +When applying styles to widgets we can use CSS to select the child widgets. Widgets rendered with the line API don't have children per-se, but we can still use CSS to apply styles to parts of our widget by defining *component classes*. Component classes are associated with a widget by defining a `COMPONENT_CLASSES` class variable which should be a `set` of strings containing CSS class names. In the checkerboard example above we hard-coded the color of the squares to "white" and "black". But what if we want to create a checkerboard with different colors? We can do this by defining two component classes, one for the "white" squares and one for the "dark" squares. This will allow us to change the colors with CSS. @@ -288,7 +288,7 @@ The following example replaces our hard-coded colors with component classes. === "checker02.py" - ```python title="checker02.py" hl_lines="13-15 18-25 37-38" + ```python title="checker02.py" hl_lines="11-13 16-23 35-36" --8<-- "docs/examples/guide/widgets/checker02.py" ``` @@ -317,7 +317,7 @@ Let's add scrolling to our checkerboard example. A standard 8 x 8 board isn't su === "checker03.py" - ```python title="checker03.py" hl_lines="26-30 35-36 52-53" + ```python title="checker03.py" hl_lines="4 26-30 35-36 52-53" --8<-- "docs/examples/guide/widgets/checker03.py" ```