From 2ce216516029c9a9fb22455bc29515bb3bbf38ab Mon Sep 17 00:00:00 2001 From: Haozhe Wu Date: Sun, 4 Oct 2026 03:09:56 +0800 Subject: [PATCH] fix(pdf): render shaped Arabic reports without clipping across pages Signed-off-by: Haozhe Wu --- agent/requirements.txt | 3 + agent/src/tools/pdf_report.py | 171 +++++++++++++++++- agent/tests/test_pdf_report_arabic.py | 140 ++++++++++++++ .../electron/requirements-windows-lock.txt | 131 ++++++++++++++ pyproject.toml | 2 + requirements-lock.txt | 131 ++++++++++++++ 6 files changed, 574 insertions(+), 4 deletions(-) create mode 100644 agent/tests/test_pdf_report_arabic.py diff --git a/agent/requirements.txt b/agent/requirements.txt index a3cc85b7..248d7992 100644 --- a/agent/requirements.txt +++ b/agent/requirements.txt @@ -70,3 +70,6 @@ nh3>=0.2.18 weasyprint>=60.0 # Pure-Python PDF fallback where weasyprint's system pango/cairo libs are missing reportlab>=4.0 +# Portable Arabic shaping and bidirectional layout for ReportLab fallback +arabic-reshaper>=3.0.1 +python-bidi>=0.6.11 diff --git a/agent/src/tools/pdf_report.py b/agent/src/tools/pdf_report.py index 0b16191d..41793c63 100644 --- a/agent/src/tools/pdf_report.py +++ b/agent/src/tools/pdf_report.py @@ -5,10 +5,173 @@ from __future__ import annotations import os import re import tempfile +import unicodedata +from functools import lru_cache +from itertools import groupby from pathlib import Path from xml.sax.saxutils import escape + +@lru_cache(maxsize=1) +def _arabic_font() -> str: + """Register the Arabic-capable font already shipped by Matplotlib.""" + import matplotlib + from reportlab.pdfbase import pdfmetrics + from reportlab.pdfbase.ttfonts import TTFont + + family = "VibeReportArabic" + path = Path(matplotlib.get_data_path()) / "fonts" / "ttf" / "DejaVuSans.ttf" + pdfmetrics.registerFont(TTFont(family, str(path))) + pdfmetrics.registerFontFamily(family, normal=family, bold=family, italic=family, boldItalic=family) + return family + + +@lru_cache(maxsize=1) +def _reshaper(): + """Keep Arabic vowel marks when shaping for visual RTL order.""" + import arabic_reshaper + + return arabic_reshaper.ArabicReshaper(configuration={ + "delete_harakat": False, "shift_harakat_position": True, + }) + + +def _visual_line(text: str, direction: str) -> str: + """Shape Arabic and apply the Unicode bidi algorithm to one logical line.""" + from bidi.algorithm import get_display + + return get_display(_reshaper().reshape(text), base_dir=direction) + + +def _font_runs(text: str, cjk: str, arabic: str) -> list[tuple[str, str]]: + """Select embedded fonts by real glyph coverage; never emit missing boxes.""" + from reportlab.pdfbase import pdfmetrics + + coverage = {family: pdfmetrics.getFont(family).face.charToGlyph for family in (arabic, cjk)} + + def select(char: str) -> str: + for family, cmap in coverage.items(): + if cmap.get(ord(char), 0): + return family + raise ValueError(f"PDF fonts do not cover U+{ord(char):04X}") + + return [(family, "".join(chars)) for family, chars in groupby(text, key=select)] + + +def _rtl_lines(text: str, width: float, size: float, cjk: str, arabic: str) -> tuple[list[tuple[str, str]], str]: + """Wrap logical text before shaping/reordering each visual line. + + Reordering the whole paragraph before wrapping reverses its line order. + Widths use the actual shaped glyphs and the font selected for each run. + """ + from bidi.algorithm import get_base_level + from reportlab.pdfbase.pdfmetrics import stringWidth + + direction = "R" if get_base_level(text) else "L" + + def fits(value: str) -> bool: + visual = _visual_line(value, direction) + return sum(stringWidth(run, family, size) for family, run in _font_runs(visual, cjk, arabic)) <= width + + lines: list[str] = [] + current = "" + for word in text.split(): + candidate = f"{current} {word}" if current else word + if fits(candidate): + current = candidate + continue + if current: + lines.append(current) + current = "" + if fits(word): + current = word + continue + # A long URL or CJK run has no word separator. Split in logical order, + # keeping combining marks attached to their base character. + clusters: list[str] = [] + for char in word: + if clusters and unicodedata.combining(char): + clusters[-1] += char + else: + clusters.append(char) + for cluster in clusters: + if current and not fits(current + cluster): + lines.append(current) + current = "" + if not fits(cluster): + raise ValueError("A PDF glyph is wider than the report column") + current += cluster + if current: + lines.append(current) + return [(line, _visual_line(line, direction)) for line in lines], direction + + +def _paragraph(markup: str, style, cjk: str): + """Keep the standard layout for LTR/CJK and reorder RTL after line breaks.""" + from reportlab.platypus import Flowable, Paragraph + + logical = Paragraph(markup, style) + text = logical.getPlainText() + if not any(unicodedata.bidirectional(char) in {"R", "AL"} for char in text): + return logical + + class BidirectionalParagraph(Flowable): + def __init__(self, prepared=None, direction=None): + super().__init__() + self.style = style + self.lines = prepared + self.direction = direction + + def wrap(self, availWidth, availHeight): + if self.lines is None: + self.lines, self.direction = _rtl_lines( + text, max(1, availWidth - style.leftIndent - style.rightIndent), + style.fontSize, cjk, _arabic_font(), + ) + self.width = availWidth + self.height = len(self.lines) * style.leading + return self.width, self.height + + def split(self, availWidth, availHeight): + self.wrap(availWidth, availHeight) + count = int(availHeight // style.leading) + if count <= 0: + return [] + first = BidirectionalParagraph(self.lines[:count], self.direction) + rest = BidirectionalParagraph(self.lines[count:], self.direction) + first.spaceAfter = 0 + rest.spaceBefore = 0 + return [first, rest] if rest.lines else [first] + + def draw(self): + from reportlab.pdfbase.pdfmetrics import stringWidth + + canvas = self.canv + canvas.saveState() + if style.backColor: + canvas.setFillColor(style.backColor) + canvas.rect(0, 0, self.width, self.height, stroke=0, fill=1) + canvas.setFillColor(style.textColor) + for index, (logical_line, visual) in enumerate(self.lines): + runs = _font_runs(visual, cjk, _arabic_font()) + width = sum(stringWidth(run, family, style.fontSize) for family, run in runs) + x = self.width - style.rightIndent - width if self.direction == "R" else style.leftIndent + y = self.height - (index + 1) * style.leading + max(0, (style.leading - style.fontSize) / 2) + # PDF ActualText supplies canonical logical text for copy/search; + # glyph positions remain the shaped visual order. Hex UTF-16BE + # keeps user text out of PDF operator syntax. + actual = ("\ufeff" + logical_line).encode("utf-16-be").hex() + canvas._code.append(f"/Span << /ActualText <{actual}> >> BDC") + for family, run in runs: + canvas.setFont(family, style.fontSize) + canvas.drawString(x, y, run) + x += stringWidth(run, family, style.fontSize) + canvas._code.append("EMC") + canvas.restoreState() + + return BidirectionalParagraph() + def render_markdown_pdf(content: str, target: Path) -> None: """Render a paginated report atomically, retaining an old file on failure. @@ -54,7 +217,7 @@ def render_markdown_pdf(content: str, target: Path) -> None: in_code = not in_code continue if in_code: - story.append(Paragraph(escape(lines[position - 1]).replace(" ", " "), code)) + story.append(_paragraph(escape(lines[position - 1]).replace(" ", " "), code, family)) continue if not line: continue @@ -65,7 +228,7 @@ def render_markdown_pdf(content: str, target: Path) -> None: rows.append([part.strip() for part in lines[position].strip().strip("|").split("|")]) position += 1 columns = max(map(len, rows)) - table = Table([[Paragraph(inline(value), cell) for value in row + [""] * (columns - len(row))] + table = Table([[_paragraph(inline(value), cell, family) for value in row + [""] * (columns - len(row))] for row in rows], colWidths=[width / columns] * columns, repeatRows=1, splitInRow=1) table.setStyle(TableStyle([ @@ -79,11 +242,11 @@ def render_markdown_pdf(content: str, target: Path) -> None: continue heading = re.match(r"^(#{1,4})\s+(.+)", line) if heading: - story.append(Paragraph(inline(heading[2]), headings[len(heading[1])])) + story.append(_paragraph(inline(heading[2]), headings[len(heading[1])], family)) else: line = re.sub(r"^[-*+]\s+", "• ", line) line = re.sub(r"^>\s?", "", line) - story.append(Paragraph(inline(line), body)) + story.append(_paragraph(inline(line), body, family)) if not story: story.append(Paragraph(" ", body)) diff --git a/agent/tests/test_pdf_report_arabic.py b/agent/tests/test_pdf_report_arabic.py new file mode 100644 index 00000000..780a21af --- /dev/null +++ b/agent/tests/test_pdf_report_arabic.py @@ -0,0 +1,140 @@ +"""Portable Arabic report rendering through real fonts, layout, and PDF bytes.""" + +from __future__ import annotations + +import unicodedata +import base64 +import re +import zlib + +import pypdfium2 as pdfium +import pytest +from reportlab.pdfbase import pdfmetrics + +from src.shadow_account.pdf_fallback import _ensure_font +from src.tools import pdf_report + + +def _text(path): + with pdfium.PdfDocument(path) as document: + return "\n".join(document[index].get_textpage().get_text_range() for index in range(len(document))) + + +def test_arabic_shapes_and_preserves_latin_numbers_and_diacritics(): + logical = "تقرير مُهِمّ: AAPL 123.45 USD و BTC-USDT 67,890.12" + visual = pdf_report._visual_line(logical, "R") + assert "AAPL 123.45 USD" in visual + assert "BTC-USDT 67,890.12" in visual + assert any(0xFE70 <= ord(char) <= 0xFEFF for char in visual) + assert {char for char in logical if unicodedata.combining(char)} <= set(visual) + assert visual != logical and visual != logical[::-1] + + +def test_every_mixed_script_glyph_has_a_real_embedded_font(): + cjk, arabic = _ensure_font(), pdf_report._arabic_font() + assert pdfmetrics.getFont(cjk).face.charToGlyph.get(ord("ت"), 0) == 0 + visual = pdf_report._visual_line("تقرير عربي 中国股票 AAPL 123.45 ١٢٣", "R") + runs = pdf_report._font_runs(visual, cjk, arabic) + assert {font for font, _ in runs} == {cjk, arabic} + for font, text in runs: + cmap = pdfmetrics.getFont(font).face.charToGlyph + assert all(cmap.get(ord(char), 0) > 0 for char in text) + + +def test_wrapping_keeps_logical_paragraph_order_and_column_width(): + from bidi.algorithm import get_display + + cjk, arabic = _ensure_font(), pdf_report._arabic_font() + source = "البداية " + "تقرير الأسواق العربية والمخاطر " * 8 + "النهاية" + lines, direction = pdf_report._rtl_lines(source, 140, 10, cjk, arabic) + assert direction == "R" and len(lines) > 3 + restored = [unicodedata.normalize("NFKC", get_display(visual, base_dir="R")) for _, visual in lines] + assert " ".join(restored) == source + assert restored[0].startswith("البداية") + assert restored[-1].endswith("النهاية") + for _, line in lines: + width = sum(pdfmetrics.stringWidth(text, font, 10) for font, text in pdf_report._font_runs(line, cjk, arabic)) + assert width <= 140 + + +def test_real_mixed_arabic_markdown_table_pdf(tmp_path): + target = tmp_path / "report.pdf" + pdf_report.render_markdown_pdf( + "# تقرير الأسواق\n\nتحليل AAPL 123.45 USD و ١٢٣\n\n" + "| السوق | السعر | Notes |\n| --- | --- | --- |\n" + "| الصين 中国 | 600519.SH | تقرير عربي |\n" + "| AAPL | 123.45 USD | 中文报告 |\n\n" + "## English / 中文\n\nA **bold** report with < & > and [source](https://example.com).\n" + "```\nprice = 123.45\n```\n", + target, + ) + data = target.read_bytes() + assert data.startswith(b"%PDF-") and data.rstrip().endswith(b"%%EOF") + assert data.count(b"/FontFile2") >= 2 and b"/ToUnicode" in data + text = _text(target) + for value in ["AAPL", "123.45 USD", "600519", "SH", "中国", "中文报告", "English", "price = 123.45"]: + assert value in text + assert "\x00" not in text and "\ufffd" not in text + # PDFium applies its own bidi heuristic across an entire table row; it + # may reorder 600519.SH into SH600519. ActualText is checked independently. + decoded = [] + for stream in re.findall(rb"stream\r?\n(.*?)endstream", data, re.S): + try: + decoded.append(zlib.decompress(base64.a85decode(stream.strip(), adobe=True))) + except (ValueError, zlib.error): + continue + canonical = ("\ufeff" + "تحليل AAPL 123.45 USD و ١٢٣").encode("utf-16-be").hex().encode() + assert b"/ActualText <" + canonical + b">" in b"\n".join(decoded) + normalized = unicodedata.normalize("NFKC", text) + assert any(unicodedata.bidirectional(char) == "AL" for char in normalized) + assert not list(tmp_path.glob(".report-*.pdf")) + + +def test_arabic_paragraph_and_tall_table_cell_paginate(tmp_path): + target = tmp_path / "long.pdf" + text = "البداية " + "تقرير الأسواق العربية AAPL 123.45 والمخاطر. " * 160 + " النهاية" + pdf_report.render_markdown_pdf( + "# تقرير طويل\n\n" + text + "\n\n| الوصف | القيمة |\n| --- | --- |\n| " + text + " | 123.45 |\n", + target, + ) + with pdfium.PdfDocument(target) as document: + assert len(document) >= 3 + extracted = _text(target) + assert extracted.count("123.45") == 321 + + +@pytest.mark.parametrize("failure", ["missing_glyph", "replace"]) +def test_arabic_render_failure_retains_old_target_and_cleans_temporary(tmp_path, monkeypatch, failure): + target = tmp_path / "report.pdf" + target.write_bytes(b"previous PDF") + content = "تقرير عربي" + if failure == "missing_glyph": + content += " \U0001fae0" # Absent from both packaged report fonts. + else: + def denied(*args): + raise OSError("replace denied") + monkeypatch.setattr(pdf_report.os, "replace", denied) + with pytest.raises((ValueError, OSError)): + pdf_report.render_markdown_pdf(content, target) + assert target.read_bytes() == b"previous PDF" + assert not list(tmp_path.glob(".report-*.pdf")) + + + +def test_tall_mixed_table_keeps_every_glyph_on_its_page(tmp_path): + target = tmp_path / "mixed-pages.pdf" + content = ("هذا تقرير للأسواق مع AAPL 123.45 USD و 中文报告 والعائد +5.25%. " * 90) + content += "\n\n| وصف | قيمة |\n| --- | --- |\n| " + content += "تقرير طويل 中文 123.45 USD. " * 160 + content += " | 123.45 |\n" + pdf_report.render_markdown_pdf(content, target) + with pdfium.PdfDocument(target) as document: + assert len(document) >= 3 + for page_number, page in enumerate(document): + width, height = page.get_size() + text = page.get_textpage() + for index in range(text.count_chars()): + left, bottom, right, top = text.get_charbox(index) + assert left >= -1 and bottom >= -1 and right <= width + 1 and top <= height + 1, ( + page_number, index, (left, bottom, right, top), (width, height) + ) diff --git a/desktop/electron/requirements-windows-lock.txt b/desktop/electron/requirements-windows-lock.txt index 2b980244..f351dce9 100644 --- a/desktop/electron/requirements-windows-lock.txt +++ b/desktop/electron/requirements-windows-lock.txt @@ -172,6 +172,10 @@ anyio==4.14.2 \ # sse-starlette # starlette # watchfiles +arabic-reshaper==3.0.1 \ + --hash=sha256:41c5adc2420f85758eada7e880251c4b6a2adbd83377bd27e5d4eba71f648bc7 \ + --hash=sha256:a0d9b2a9fa29b5f2c1d705f407adf6ca4242405b9cac0e5cc09e6c4f3f8fb68c + # via -r agent/requirements.txt attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 @@ -2921,6 +2925,133 @@ pyphen==0.17.2 \ --hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \ --hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3 # via weasyprint +python-bidi==0.6.11 \ + --hash=sha256:034090c597af250d699299d7e7f1e83eb016f9e47b3b707bd89ab2bdec77bce0 \ + --hash=sha256:0608bddcc1c53dfa5293499de13ca9935b31aa46d1c722c404a88c703d1a4e47 \ + --hash=sha256:0d339c4ebf3c2501d7569971a22e762fb45d8af7e19a0573afe260d69b35fb0d \ + --hash=sha256:0d496f9fc21b7457e12395e54088ab99776966c663b4cd4a74770c7a6418ab59 \ + --hash=sha256:19bae96ff1ee76b3a7dc962598b69426538e3021460cf85a4017437548ab6947 \ + --hash=sha256:19d13c70b1f7bb5dc69866a11624f3b78123807b4ede18f4f99f656ed86babec \ + --hash=sha256:1a639a2c62f825e72eb42901fd1c69296d09c192ae003195e6cf773f92d6bb42 \ + --hash=sha256:1ad5f712a32be30d28eb96e119e85818747a43cea253de6c3160b464b62a5619 \ + --hash=sha256:1b41cc6bc9ad78a12da5f987da15e931c771f18ceca58f2fe8ed50f253490a97 \ + --hash=sha256:1ffd728f1f7866ff7399906bbc17ef5cf010b90ce56a1e94937b28a1a4cf5a7d \ + --hash=sha256:20ab47a4098577fc9a82816c330c89ff597c31ba69c98bc6a1b6a5737b03de05 \ + --hash=sha256:239a00b2adb5f897d11d7b7a491d759fb9883e71a71cfd90c4147a733b4df4d3 \ + --hash=sha256:2b208fc26ab2c7adfd2c4d84753a5045f664e0bec19ca462af2406132e62f92e \ + --hash=sha256:2d6970b09f5a3102c0aa192f5258c585742ab4ebd94f637a635ad3448ccba567 \ + --hash=sha256:2ff75d1befc335cbe85f834e81554a024f94d9b5d1dd75a5bd99af81a1cb783c \ + --hash=sha256:302f4ca7dabfe447e707d40e98520551181036c15750bdd1e73292ad8b3d8e75 \ + --hash=sha256:30d543b5baf9fca5ef5ec95647aa07c5e38fc7fa0f18be0c61d1d6c0a1032c6c \ + --hash=sha256:337ec315f8b286399e411f5bfc339fc2124db07757409b0595ceb46de087c4cd \ + --hash=sha256:3611d13b53d4c899c4f2a7cd8eb897064e8b5546c3c5d1037dd6209c82858a27 \ + --hash=sha256:36f23f12d9b1c56ed8d82e11f56c6cba7bc3f614ee73374bc7772bb2270e0966 \ + --hash=sha256:380b70d615647646dbe06f7d95dc30b8fdc9b596dbfa1cd3814feb9805c0c8f2 \ + --hash=sha256:397f7f289eba6ce25d99dbea99f873d699bf9aa030074e7fb746d8f93c2fb6f9 \ + --hash=sha256:3bdb64ee0a74951465cd4a761e1029e73806ff43b2fe5be98643e52da5cabb66 \ + --hash=sha256:3da9e536546f7c62c0da595c8f71a096e0b9a80e94cfd0f329b7b200ef81e7d5 \ + --hash=sha256:41c7d1914c1f33954aa2ab0e2c309be5d1d4afc75ce524762eaab4dd825c5ab8 \ + --hash=sha256:43f3e81bdd36f49171b7de6cf471086df503c29555f6f7f035ebe8f8ec1da779 \ + --hash=sha256:490118f4af5a3535923c76be07e937ef1cb4cf84c489b37471aa95455fe923b3 \ + --hash=sha256:493655043ebea6fec0edb76053bdad46f1f7352a759ca4deb733f13b3c09839c \ + --hash=sha256:495a76c881d78ab87b57c5270679c9bc3c1de36d8c6596d5e3a5a1b5f9c57471 \ + --hash=sha256:4ac819cac1abb15486c48af3399a5c726e89f0977a3aff205ac162533186e756 \ + --hash=sha256:4ccf1fe9ecb3b02a1a11b103cd2557e2653d82c141b6e2dccec8177e6af5c4bb \ + --hash=sha256:4d00757ef7bbbf14d8628f9bdb6b0e168d5e7b03fec20da3226624f11bffce89 \ + --hash=sha256:51915502898c45e9cb36636e974aca068fc5cdb9f06b794f49abea5b12f02016 \ + --hash=sha256:519eb90bedb04b5cbba0bfc8062984a5ba054a019d81ada34053dc67397d61cb \ + --hash=sha256:539d99efe02f4981171ed57bbc085094ef780405ca14663550a56c6c3e265c34 \ + --hash=sha256:555cdf9303c40bae1ab512ca427f1f0316a574bc0a48db22eec76ec0fd1213cf \ + --hash=sha256:55f27bd2cdeca96f46448a741d393575fbedfc70e38e09dbb030ed98dba8cf2a \ + --hash=sha256:55fa5b22cc8c220a6daf077570a8e29d3b607bd978bd3dbc04b445d01a17de57 \ + --hash=sha256:565d819fddb2bbc58c42ca5c97d73da7567f181115011e8f04c76b9d08378dcd \ + --hash=sha256:56807e0dde88d5ab96880c9d668bda7bca1df83cd30d20cbff5d6b6e6c1e9276 \ + --hash=sha256:56dfa3c1c13cb89da39a4e8f2f9442e4291de01877511a13f830125d36e9ce5b \ + --hash=sha256:56f27c1edfd15c12c9c348378ccd79166930d720cf316b1181a0a0ade2146253 \ + --hash=sha256:57aa56b1eca5f63ebdd25fe8fad02eab7797fc45ad1efc5eed2a830e2bd2038d \ + --hash=sha256:5dd49c11ae87ee6ced68594faa78b34a89a1a3c43647fa6157765f9726f9daf3 \ + --hash=sha256:5f3e1743b2d43377c4da5d687a03430a27f5769e158a9f75a50b05e7e82f4d21 \ + --hash=sha256:5f9ed312d445a691c0afec995ca4f726abe357d8da500e389f77fb874fa7ef76 \ + --hash=sha256:60150bffbe43bfc1e356c0c2900ba643a02ce09c37bc0c553c4d83e5c3de63ad \ + --hash=sha256:60d9bf6c60c022657637f64897e63dc3f5b1c07cb7f0e1ead6150aff5150c5ab \ + --hash=sha256:619ee3fe03daec8d3ce12239f0c22455676a063b2bcde361caecd788fae5b8d5 \ + --hash=sha256:62a6b700f3d7a2c4d52a5dccca765711a2414e734360b00365e155698a26e461 \ + --hash=sha256:63d9dcc714d549a5118ebc93b7a7c903a0c1feec8e57f5fcacf98d984b76325b \ + --hash=sha256:6623683fe39b9fbf508e3069f17e8e9cab26143f9d9f89c8a8f45424c052df4f \ + --hash=sha256:68c3e570f2908ac39db0b269f5648e257fab5204344b9bf6dae80852ae4cbe1c \ + --hash=sha256:69d1f4ee17644e8aeac93a7238ee2f28d79b0180815441eb511b77e6585aa971 \ + --hash=sha256:6c92d1cad16f9ec2f2a3ae439a0bc3a8e4189ec227987bed03d5b4056d5eb9c5 \ + --hash=sha256:6ca92a4e460f7e25e434a6d7982d94a4765ca242f527995da70abb5a32003b8a \ + --hash=sha256:6d9ef69c108b31f38e1f281a55fdedad7774bc1e952a45c8c14a18e891eee397 \ + --hash=sha256:6dc112e2239f69913273cbb0c050ca816b145b32037d4266c430159c5ddcdab2 \ + --hash=sha256:721697187f4da67dafc26f63488f463fa35ff2de8668d959fda773cccdbef0eb \ + --hash=sha256:73c38c604bfc01647c69ce38e5cf8b4206e2ede91ca3eb8e5d79b7409f17e0b3 \ + --hash=sha256:74457b43db34f984252e915828b5d1a4042a771f44e853a5643506d01562eccb \ + --hash=sha256:7738cfc7ee9fcdff3fef76b40007982ce7704010a51df307c4a51d378f5ff1ba \ + --hash=sha256:777fb44a6f1e91d6adde36e815024c210b0208f35d00762880257b8fb04dc849 \ + --hash=sha256:788c11c84b520ea973992cc95751d56b783ff5857df504c1347d99cacd2fcfe7 \ + --hash=sha256:79df1099a08e53edb678236d4d76d8de4e3901bafc84ce1788b71f9b96547325 \ + --hash=sha256:7c4dc0d1328321aeb08054be654e1d39f171d5aae8dca931c9332860f18f57d7 \ + --hash=sha256:7cb34dd8d22ca1847653c0e1cbba8fd96de09c24b8c83b7805eab270c81e863a \ + --hash=sha256:8013c1e6267a929be98d10c3a617171709c8226bd33c6180ec65e560c35b6df8 \ + --hash=sha256:80cfe97e2d65981be877ae5bd2338c6919a7cc1171fc308c8b8c7c306ba6ffd8 \ + --hash=sha256:83c780f7e4c3dd3f020db75dc425567981e65c6d5571b3c0372203d0df92c834 \ + --hash=sha256:83ee87feb5eafc0442e1db0014dad20d52a2a7a140b6cddc8f7bc65918f0a7b4 \ + --hash=sha256:879c3fba3e7511c7d01020449d970ca7d6a593f20cfc47c014d3d229a38930b2 \ + --hash=sha256:8b6b7fce8f47578be9aebf5a0b6b2d6c157b4e97af7586ecf13bfca5d128deea \ + --hash=sha256:8eb09af209cd660fa9689f6ce9e61e73c8afa4829ae61801deea7f6e32263800 \ + --hash=sha256:8fbb6d222b50324fb9d49b6ff0f8566fa97b907a68c00e6622fcf34463104f4a \ + --hash=sha256:9435acc52438c3c8f5142b9a17a622927618e80a32eed707343bc375cb51cebe \ + --hash=sha256:946b7dbec4e64017680f1a66b3a8534659d889018ac83bd2abf958278e6f62b0 \ + --hash=sha256:94f7f66bd7680d3f2085adaeea91c01b3281a79bb5041db1a51cbe8af36a9d88 \ + --hash=sha256:959335cd3814cb767fb832c5c71cbc838ccd9231a812ef2cb43092a216a91d5b \ + --hash=sha256:969ee7db3e169fcc0b2d2d094826e03cc5798dfd6b3571a340ea883672396cb1 \ + --hash=sha256:9af2b5c26a3eb960699dff040535a86dc2c0f708087b2d63bcfd6452fe9d0664 \ + --hash=sha256:9d18916c97855d68515ca2b3581948b6d864c304cd386210ab5aff5e0fb9be2c \ + --hash=sha256:a1115f3f02eb836b39e0c986a6b9e92c4377a1e5a680409650b02ec6b1a795e6 \ + --hash=sha256:a1b5ee069001bf7f4fff109598a9396caa96bb35e1b906b2c6d1bab9f9b2c4bd \ + --hash=sha256:a4f4a0cd24c09df748bfdc207b089c00e7af19f3151063f4cd74ac658290186b \ + --hash=sha256:a52f7ad9ef9091e81869e5d255e796755ccf542ade14dda17647cb7d7ffe1b9c \ + --hash=sha256:a873e6f8ab28b5a56201d5cdb98f4c7d2ff88e636a58659c5ac271e287001658 \ + --hash=sha256:a9aa2890661b238730e680ccd6eb06f4da625b2dbc1730052dae6f2d88957192 \ + --hash=sha256:ab8af1cc29c3f400a0f781d50f8ab52a8c63adf2fcef16f14641cd97e06a80e0 \ + --hash=sha256:acccb6d90e694684db2d314db2e2b0d3b8949bf1cfaec6d9808a8889f548add7 \ + --hash=sha256:ad5d9b8e8a6c330208eba413db506de58f21dbf88a1f1d5d75ef5f9e0e714adf \ + --hash=sha256:af7711cc6eeeadcb1aae877e0736a68e111c73a29562da6106c5e2fbb4dd83b2 \ + --hash=sha256:b2c759e13ebb81edaac3041697328bb1ca8433b55281723879f6b54e74881240 \ + --hash=sha256:b433840a924c8788f0abbda15d22c71dc636e2078d7d3ba39369cebe4bef74b8 \ + --hash=sha256:bde89739a979d9eb3ac48c4882c8a42dd528a7708b0faa99b90b551588bd5f8d \ + --hash=sha256:c17393753ddb77377f754ba2a88cacfe3056040a8a5dff8d43f9d5e51bbe1edc \ + --hash=sha256:c26cd9d81f820159026b1c99905ca12bf13892e3f6a9303359fef42fe6f39e50 \ + --hash=sha256:c6aca20472d7040bc1f0aadb510cb20d40b3d6caa4c8bf82d075ba2650a41652 \ + --hash=sha256:c6b3c853f99172ef22e5a16c8114cf243e351c8e70f72a894164088c2c99d9cb \ + --hash=sha256:caee7ee3662eab1411b44fef8571b87273cb5235061d7463ebd10e412ac07986 \ + --hash=sha256:cd564b8c583eba2d230d02d0467fd840045f08856b6524555cfff7f32af63c72 \ + --hash=sha256:ce2ef68dce82ae4067cf28910224d77b45eb2c2b3904db6eb670d1cddb8d0eb2 \ + --hash=sha256:cf4e88a6fec81b7155a487cbbea7753a3d9a76dc4d391b4f8958b37227ef2c12 \ + --hash=sha256:d14f2d400c75e07d1154299463a3d4d14aa5565b05088b2d9b314ccd9fd6dc3a \ + --hash=sha256:d52ec8ccdc2fd5c61749d876a9d1eb0ea6543c1676722e1e3fb9d7800852131c \ + --hash=sha256:d5695d87969fed5b3799b8a98848cb04fe2873fded46efe9f3f2f341efd1b829 \ + --hash=sha256:d7291e13496cb74fc1b71f7f1e3628586afefa531102bb4fa7af9c2d543efc3c \ + --hash=sha256:d73a821873c52635321196cfb8d3a231d7917ca284cf2bcd9422f6deb19db7ca \ + --hash=sha256:da51a3a2940478219f19249fcf7cd45e8ddc197982be22efa43c4763e9d2eb57 \ + --hash=sha256:dc69d594682fbedec7d0cc81cd4d5f14fb40b6b365587238fafb91509cfea020 \ + --hash=sha256:dfbb9ba8343a60daf4ced67c11d551dafe9a3c94892c326e4c216fa2e6eca802 \ + --hash=sha256:e142820537e08d567c2c017e26df05f88856a03f0f3948209cfcdc39617df363 \ + --hash=sha256:e4ebc24ac38e50676f65daf7ba6c568789660cb60d6dcf2606d4310dba826721 \ + --hash=sha256:e6c474618a7b6a50c10007f8a9edb50def6d98d297a07a34dc2fb82c344f2b8b \ + --hash=sha256:f0ee4e80dd3b2a46868dcc73e00aa59f99c0e1de54a2ba783faf0fddba2ccce2 \ + --hash=sha256:f237ebb570fd8bbe479b6967374d82b7f0b26f9452c276bdd5f793d83e7062bd \ + --hash=sha256:f400a30573774a1e90c0d50d43c35d9c004afcf53de801bbfd259f84ea80f31c \ + --hash=sha256:f563d20481f7d316adf605bb94d5b7182acecdbc4d431d60473e9b1d526d0210 \ + --hash=sha256:f7a8429d0d65232e314b4f494825c1205abcc3039f42bf7da80424a15b731709 \ + --hash=sha256:f8655ac559dcfce7179b150ebcc207982d4d60e67b3089b2032eed3a0a78c07a \ + --hash=sha256:f99162d6c6c9522c46eb213f1bc932829c2602131676c92f081cb865b8ef6784 \ + --hash=sha256:fa2848c3116d619870d114471fcd4a9f1aa43587a002111d7af1e6582f1b57e9 \ + --hash=sha256:fc3b0a6e2460f68de9ab98f71e3098bb21bb984563417ad104dec7ab08ebcadc \ + --hash=sha256:fccd1808bb427d6a6d34168461bef551faa93ce3dda489fcef9073bcd9b34a5e \ + --hash=sha256:ff675d036823bff05a2e0f8cdb13f5414274ff517d13b0d57c15527015eb6acb + # via -r agent/requirements.txt python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 diff --git a/pyproject.toml b/pyproject.toml index 460cec40..1f81d598 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,8 @@ dependencies = [ # ships binary wheels everywhere and backs the pure-Python PDF fallback # (shadow_account/pdf_fallback.py) so the PDF renders on any host. "reportlab>=4.0", + "arabic-reshaper>=3.0.1", + "python-bidi>=0.6.11", "prompt_toolkit>=3.0.0", ] diff --git a/requirements-lock.txt b/requirements-lock.txt index 6b39f74f..8703d38c 100644 --- a/requirements-lock.txt +++ b/requirements-lock.txt @@ -172,6 +172,10 @@ anyio==4.14.2 \ # sse-starlette # starlette # watchfiles +arabic-reshaper==3.0.1 \ + --hash=sha256:41c5adc2420f85758eada7e880251c4b6a2adbd83377bd27e5d4eba71f648bc7 \ + --hash=sha256:a0d9b2a9fa29b5f2c1d705f407adf6ca4242405b9cac0e5cc09e6c4f3f8fb68c + # via -r agent/requirements.txt attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 @@ -2974,6 +2978,133 @@ pyphen==0.18.1 \ --hash=sha256:0aa9051e15928cecadd4c632cea0258ba57215b2a197a39baa46abcdb0f47e84 \ --hash=sha256:dbae6fbbe4f01cb206108b43573d857c67107be9d0e38eb1b08d6fa2210634a7 # via weasyprint +python-bidi==0.6.11 \ + --hash=sha256:034090c597af250d699299d7e7f1e83eb016f9e47b3b707bd89ab2bdec77bce0 \ + --hash=sha256:0608bddcc1c53dfa5293499de13ca9935b31aa46d1c722c404a88c703d1a4e47 \ + --hash=sha256:0d339c4ebf3c2501d7569971a22e762fb45d8af7e19a0573afe260d69b35fb0d \ + --hash=sha256:0d496f9fc21b7457e12395e54088ab99776966c663b4cd4a74770c7a6418ab59 \ + --hash=sha256:19bae96ff1ee76b3a7dc962598b69426538e3021460cf85a4017437548ab6947 \ + --hash=sha256:19d13c70b1f7bb5dc69866a11624f3b78123807b4ede18f4f99f656ed86babec \ + --hash=sha256:1a639a2c62f825e72eb42901fd1c69296d09c192ae003195e6cf773f92d6bb42 \ + --hash=sha256:1ad5f712a32be30d28eb96e119e85818747a43cea253de6c3160b464b62a5619 \ + --hash=sha256:1b41cc6bc9ad78a12da5f987da15e931c771f18ceca58f2fe8ed50f253490a97 \ + --hash=sha256:1ffd728f1f7866ff7399906bbc17ef5cf010b90ce56a1e94937b28a1a4cf5a7d \ + --hash=sha256:20ab47a4098577fc9a82816c330c89ff597c31ba69c98bc6a1b6a5737b03de05 \ + --hash=sha256:239a00b2adb5f897d11d7b7a491d759fb9883e71a71cfd90c4147a733b4df4d3 \ + --hash=sha256:2b208fc26ab2c7adfd2c4d84753a5045f664e0bec19ca462af2406132e62f92e \ + --hash=sha256:2d6970b09f5a3102c0aa192f5258c585742ab4ebd94f637a635ad3448ccba567 \ + --hash=sha256:2ff75d1befc335cbe85f834e81554a024f94d9b5d1dd75a5bd99af81a1cb783c \ + --hash=sha256:302f4ca7dabfe447e707d40e98520551181036c15750bdd1e73292ad8b3d8e75 \ + --hash=sha256:30d543b5baf9fca5ef5ec95647aa07c5e38fc7fa0f18be0c61d1d6c0a1032c6c \ + --hash=sha256:337ec315f8b286399e411f5bfc339fc2124db07757409b0595ceb46de087c4cd \ + --hash=sha256:3611d13b53d4c899c4f2a7cd8eb897064e8b5546c3c5d1037dd6209c82858a27 \ + --hash=sha256:36f23f12d9b1c56ed8d82e11f56c6cba7bc3f614ee73374bc7772bb2270e0966 \ + --hash=sha256:380b70d615647646dbe06f7d95dc30b8fdc9b596dbfa1cd3814feb9805c0c8f2 \ + --hash=sha256:397f7f289eba6ce25d99dbea99f873d699bf9aa030074e7fb746d8f93c2fb6f9 \ + --hash=sha256:3bdb64ee0a74951465cd4a761e1029e73806ff43b2fe5be98643e52da5cabb66 \ + --hash=sha256:3da9e536546f7c62c0da595c8f71a096e0b9a80e94cfd0f329b7b200ef81e7d5 \ + --hash=sha256:41c7d1914c1f33954aa2ab0e2c309be5d1d4afc75ce524762eaab4dd825c5ab8 \ + --hash=sha256:43f3e81bdd36f49171b7de6cf471086df503c29555f6f7f035ebe8f8ec1da779 \ + --hash=sha256:490118f4af5a3535923c76be07e937ef1cb4cf84c489b37471aa95455fe923b3 \ + --hash=sha256:493655043ebea6fec0edb76053bdad46f1f7352a759ca4deb733f13b3c09839c \ + --hash=sha256:495a76c881d78ab87b57c5270679c9bc3c1de36d8c6596d5e3a5a1b5f9c57471 \ + --hash=sha256:4ac819cac1abb15486c48af3399a5c726e89f0977a3aff205ac162533186e756 \ + --hash=sha256:4ccf1fe9ecb3b02a1a11b103cd2557e2653d82c141b6e2dccec8177e6af5c4bb \ + --hash=sha256:4d00757ef7bbbf14d8628f9bdb6b0e168d5e7b03fec20da3226624f11bffce89 \ + --hash=sha256:51915502898c45e9cb36636e974aca068fc5cdb9f06b794f49abea5b12f02016 \ + --hash=sha256:519eb90bedb04b5cbba0bfc8062984a5ba054a019d81ada34053dc67397d61cb \ + --hash=sha256:539d99efe02f4981171ed57bbc085094ef780405ca14663550a56c6c3e265c34 \ + --hash=sha256:555cdf9303c40bae1ab512ca427f1f0316a574bc0a48db22eec76ec0fd1213cf \ + --hash=sha256:55f27bd2cdeca96f46448a741d393575fbedfc70e38e09dbb030ed98dba8cf2a \ + --hash=sha256:55fa5b22cc8c220a6daf077570a8e29d3b607bd978bd3dbc04b445d01a17de57 \ + --hash=sha256:565d819fddb2bbc58c42ca5c97d73da7567f181115011e8f04c76b9d08378dcd \ + --hash=sha256:56807e0dde88d5ab96880c9d668bda7bca1df83cd30d20cbff5d6b6e6c1e9276 \ + --hash=sha256:56dfa3c1c13cb89da39a4e8f2f9442e4291de01877511a13f830125d36e9ce5b \ + --hash=sha256:56f27c1edfd15c12c9c348378ccd79166930d720cf316b1181a0a0ade2146253 \ + --hash=sha256:57aa56b1eca5f63ebdd25fe8fad02eab7797fc45ad1efc5eed2a830e2bd2038d \ + --hash=sha256:5dd49c11ae87ee6ced68594faa78b34a89a1a3c43647fa6157765f9726f9daf3 \ + --hash=sha256:5f3e1743b2d43377c4da5d687a03430a27f5769e158a9f75a50b05e7e82f4d21 \ + --hash=sha256:5f9ed312d445a691c0afec995ca4f726abe357d8da500e389f77fb874fa7ef76 \ + --hash=sha256:60150bffbe43bfc1e356c0c2900ba643a02ce09c37bc0c553c4d83e5c3de63ad \ + --hash=sha256:60d9bf6c60c022657637f64897e63dc3f5b1c07cb7f0e1ead6150aff5150c5ab \ + --hash=sha256:619ee3fe03daec8d3ce12239f0c22455676a063b2bcde361caecd788fae5b8d5 \ + --hash=sha256:62a6b700f3d7a2c4d52a5dccca765711a2414e734360b00365e155698a26e461 \ + --hash=sha256:63d9dcc714d549a5118ebc93b7a7c903a0c1feec8e57f5fcacf98d984b76325b \ + --hash=sha256:6623683fe39b9fbf508e3069f17e8e9cab26143f9d9f89c8a8f45424c052df4f \ + --hash=sha256:68c3e570f2908ac39db0b269f5648e257fab5204344b9bf6dae80852ae4cbe1c \ + --hash=sha256:69d1f4ee17644e8aeac93a7238ee2f28d79b0180815441eb511b77e6585aa971 \ + --hash=sha256:6c92d1cad16f9ec2f2a3ae439a0bc3a8e4189ec227987bed03d5b4056d5eb9c5 \ + --hash=sha256:6ca92a4e460f7e25e434a6d7982d94a4765ca242f527995da70abb5a32003b8a \ + --hash=sha256:6d9ef69c108b31f38e1f281a55fdedad7774bc1e952a45c8c14a18e891eee397 \ + --hash=sha256:6dc112e2239f69913273cbb0c050ca816b145b32037d4266c430159c5ddcdab2 \ + --hash=sha256:721697187f4da67dafc26f63488f463fa35ff2de8668d959fda773cccdbef0eb \ + --hash=sha256:73c38c604bfc01647c69ce38e5cf8b4206e2ede91ca3eb8e5d79b7409f17e0b3 \ + --hash=sha256:74457b43db34f984252e915828b5d1a4042a771f44e853a5643506d01562eccb \ + --hash=sha256:7738cfc7ee9fcdff3fef76b40007982ce7704010a51df307c4a51d378f5ff1ba \ + --hash=sha256:777fb44a6f1e91d6adde36e815024c210b0208f35d00762880257b8fb04dc849 \ + --hash=sha256:788c11c84b520ea973992cc95751d56b783ff5857df504c1347d99cacd2fcfe7 \ + --hash=sha256:79df1099a08e53edb678236d4d76d8de4e3901bafc84ce1788b71f9b96547325 \ + --hash=sha256:7c4dc0d1328321aeb08054be654e1d39f171d5aae8dca931c9332860f18f57d7 \ + --hash=sha256:7cb34dd8d22ca1847653c0e1cbba8fd96de09c24b8c83b7805eab270c81e863a \ + --hash=sha256:8013c1e6267a929be98d10c3a617171709c8226bd33c6180ec65e560c35b6df8 \ + --hash=sha256:80cfe97e2d65981be877ae5bd2338c6919a7cc1171fc308c8b8c7c306ba6ffd8 \ + --hash=sha256:83c780f7e4c3dd3f020db75dc425567981e65c6d5571b3c0372203d0df92c834 \ + --hash=sha256:83ee87feb5eafc0442e1db0014dad20d52a2a7a140b6cddc8f7bc65918f0a7b4 \ + --hash=sha256:879c3fba3e7511c7d01020449d970ca7d6a593f20cfc47c014d3d229a38930b2 \ + --hash=sha256:8b6b7fce8f47578be9aebf5a0b6b2d6c157b4e97af7586ecf13bfca5d128deea \ + --hash=sha256:8eb09af209cd660fa9689f6ce9e61e73c8afa4829ae61801deea7f6e32263800 \ + --hash=sha256:8fbb6d222b50324fb9d49b6ff0f8566fa97b907a68c00e6622fcf34463104f4a \ + --hash=sha256:9435acc52438c3c8f5142b9a17a622927618e80a32eed707343bc375cb51cebe \ + --hash=sha256:946b7dbec4e64017680f1a66b3a8534659d889018ac83bd2abf958278e6f62b0 \ + --hash=sha256:94f7f66bd7680d3f2085adaeea91c01b3281a79bb5041db1a51cbe8af36a9d88 \ + --hash=sha256:959335cd3814cb767fb832c5c71cbc838ccd9231a812ef2cb43092a216a91d5b \ + --hash=sha256:969ee7db3e169fcc0b2d2d094826e03cc5798dfd6b3571a340ea883672396cb1 \ + --hash=sha256:9af2b5c26a3eb960699dff040535a86dc2c0f708087b2d63bcfd6452fe9d0664 \ + --hash=sha256:9d18916c97855d68515ca2b3581948b6d864c304cd386210ab5aff5e0fb9be2c \ + --hash=sha256:a1115f3f02eb836b39e0c986a6b9e92c4377a1e5a680409650b02ec6b1a795e6 \ + --hash=sha256:a1b5ee069001bf7f4fff109598a9396caa96bb35e1b906b2c6d1bab9f9b2c4bd \ + --hash=sha256:a4f4a0cd24c09df748bfdc207b089c00e7af19f3151063f4cd74ac658290186b \ + --hash=sha256:a52f7ad9ef9091e81869e5d255e796755ccf542ade14dda17647cb7d7ffe1b9c \ + --hash=sha256:a873e6f8ab28b5a56201d5cdb98f4c7d2ff88e636a58659c5ac271e287001658 \ + --hash=sha256:a9aa2890661b238730e680ccd6eb06f4da625b2dbc1730052dae6f2d88957192 \ + --hash=sha256:ab8af1cc29c3f400a0f781d50f8ab52a8c63adf2fcef16f14641cd97e06a80e0 \ + --hash=sha256:acccb6d90e694684db2d314db2e2b0d3b8949bf1cfaec6d9808a8889f548add7 \ + --hash=sha256:ad5d9b8e8a6c330208eba413db506de58f21dbf88a1f1d5d75ef5f9e0e714adf \ + --hash=sha256:af7711cc6eeeadcb1aae877e0736a68e111c73a29562da6106c5e2fbb4dd83b2 \ + --hash=sha256:b2c759e13ebb81edaac3041697328bb1ca8433b55281723879f6b54e74881240 \ + --hash=sha256:b433840a924c8788f0abbda15d22c71dc636e2078d7d3ba39369cebe4bef74b8 \ + --hash=sha256:bde89739a979d9eb3ac48c4882c8a42dd528a7708b0faa99b90b551588bd5f8d \ + --hash=sha256:c17393753ddb77377f754ba2a88cacfe3056040a8a5dff8d43f9d5e51bbe1edc \ + --hash=sha256:c26cd9d81f820159026b1c99905ca12bf13892e3f6a9303359fef42fe6f39e50 \ + --hash=sha256:c6aca20472d7040bc1f0aadb510cb20d40b3d6caa4c8bf82d075ba2650a41652 \ + --hash=sha256:c6b3c853f99172ef22e5a16c8114cf243e351c8e70f72a894164088c2c99d9cb \ + --hash=sha256:caee7ee3662eab1411b44fef8571b87273cb5235061d7463ebd10e412ac07986 \ + --hash=sha256:cd564b8c583eba2d230d02d0467fd840045f08856b6524555cfff7f32af63c72 \ + --hash=sha256:ce2ef68dce82ae4067cf28910224d77b45eb2c2b3904db6eb670d1cddb8d0eb2 \ + --hash=sha256:cf4e88a6fec81b7155a487cbbea7753a3d9a76dc4d391b4f8958b37227ef2c12 \ + --hash=sha256:d14f2d400c75e07d1154299463a3d4d14aa5565b05088b2d9b314ccd9fd6dc3a \ + --hash=sha256:d52ec8ccdc2fd5c61749d876a9d1eb0ea6543c1676722e1e3fb9d7800852131c \ + --hash=sha256:d5695d87969fed5b3799b8a98848cb04fe2873fded46efe9f3f2f341efd1b829 \ + --hash=sha256:d7291e13496cb74fc1b71f7f1e3628586afefa531102bb4fa7af9c2d543efc3c \ + --hash=sha256:d73a821873c52635321196cfb8d3a231d7917ca284cf2bcd9422f6deb19db7ca \ + --hash=sha256:da51a3a2940478219f19249fcf7cd45e8ddc197982be22efa43c4763e9d2eb57 \ + --hash=sha256:dc69d594682fbedec7d0cc81cd4d5f14fb40b6b365587238fafb91509cfea020 \ + --hash=sha256:dfbb9ba8343a60daf4ced67c11d551dafe9a3c94892c326e4c216fa2e6eca802 \ + --hash=sha256:e142820537e08d567c2c017e26df05f88856a03f0f3948209cfcdc39617df363 \ + --hash=sha256:e4ebc24ac38e50676f65daf7ba6c568789660cb60d6dcf2606d4310dba826721 \ + --hash=sha256:e6c474618a7b6a50c10007f8a9edb50def6d98d297a07a34dc2fb82c344f2b8b \ + --hash=sha256:f0ee4e80dd3b2a46868dcc73e00aa59f99c0e1de54a2ba783faf0fddba2ccce2 \ + --hash=sha256:f237ebb570fd8bbe479b6967374d82b7f0b26f9452c276bdd5f793d83e7062bd \ + --hash=sha256:f400a30573774a1e90c0d50d43c35d9c004afcf53de801bbfd259f84ea80f31c \ + --hash=sha256:f563d20481f7d316adf605bb94d5b7182acecdbc4d431d60473e9b1d526d0210 \ + --hash=sha256:f7a8429d0d65232e314b4f494825c1205abcc3039f42bf7da80424a15b731709 \ + --hash=sha256:f8655ac559dcfce7179b150ebcc207982d4d60e67b3089b2032eed3a0a78c07a \ + --hash=sha256:f99162d6c6c9522c46eb213f1bc932829c2602131676c92f081cb865b8ef6784 \ + --hash=sha256:fa2848c3116d619870d114471fcd4a9f1aa43587a002111d7af1e6582f1b57e9 \ + --hash=sha256:fc3b0a6e2460f68de9ab98f71e3098bb21bb984563417ad104dec7ab08ebcadc \ + --hash=sha256:fccd1808bb427d6a6d34168461bef551faa93ce3dda489fcef9073bcd9b34a5e \ + --hash=sha256:ff675d036823bff05a2e0f8cdb13f5414274ff517d13b0d57c15527015eb6acb + # via -r agent/requirements.txt python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427