From 18f669ba8e2865fa90a2f9da8e6357706159578c Mon Sep 17 00:00:00 2001 From: Tobias Genannt Date: Thu, 14 Aug 2025 19:53:06 +0200 Subject: [PATCH] Closes #20013: Syntax highlighting on Data Files Add syntax highlighting on the details page for Data Files. --- base_requirements.txt | 4 + netbox/core/templatetags/__init__.py | 0 netbox/core/templatetags/highlight_code.py | 30 ++ netbox/core/tests/test_templatetags.py | 49 ++++ netbox/project-static/dist/netbox.css | Bin 555435 -> 563920 bytes netbox/project-static/styles/netbox.scss | 4 + .../project-static/styles/pygments/README.md | 33 +++ .../project-static/styles/pygments/_dark.scss | 277 ++++++++++++++++++ .../styles/pygments/_light.scss | 277 ++++++++++++++++++ netbox/templates/core/datafile.html | 3 +- requirements.txt | 1 + 11 files changed, 677 insertions(+), 1 deletion(-) create mode 100644 netbox/core/templatetags/__init__.py create mode 100644 netbox/core/templatetags/highlight_code.py create mode 100644 netbox/core/tests/test_templatetags.py create mode 100644 netbox/project-static/styles/pygments/README.md create mode 100644 netbox/project-static/styles/pygments/_dark.scss create mode 100644 netbox/project-static/styles/pygments/_light.scss diff --git a/base_requirements.txt b/base_requirements.txt index d11eff972..9bc2ff9ff 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -121,6 +121,10 @@ Pillow # https://github.com/psycopg/psycopg/blob/master/docs/news.rst psycopg[c,pool] +# Pygments syntax highlighting +# https://github.com/pygments/pygments/blob/master/CHANGES +Pygments + # YAML rendering library # https://github.com/yaml/pyyaml/blob/master/CHANGES PyYAML diff --git a/netbox/core/templatetags/__init__.py b/netbox/core/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/templatetags/highlight_code.py b/netbox/core/templatetags/highlight_code.py new file mode 100644 index 000000000..237a47a71 --- /dev/null +++ b/netbox/core/templatetags/highlight_code.py @@ -0,0 +1,30 @@ +from django import template +from django.utils.safestring import mark_safe +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import get_lexer_for_filename +from pygments.util import ClassNotFound + +register = template.Library() + + +@register.simple_tag +def highlight_code(value, filename: str): + """ + Highlight code using Pygments. + """ + if not value: + return mark_safe('
')
+    if not filename:
+        return mark_safe(f'
{value}
') # Fallback to plain text if no filename is provided + try: + lexer = get_lexer_for_filename(filename) + except ClassNotFound: + return mark_safe(f'
{value}
') # Fallback to plain text if no lexer was found + return mark_safe( + highlight( + value, + lexer, + HtmlFormatter(linenos='inline', classprefix='pygments-', style='solarized-light'), + ) + ) diff --git a/netbox/core/tests/test_templatetags.py b/netbox/core/tests/test_templatetags.py new file mode 100644 index 000000000..0a1f9ce54 --- /dev/null +++ b/netbox/core/tests/test_templatetags.py @@ -0,0 +1,49 @@ +# ruff: noqa: E501 +from django.test import TestCase +from core.templatetags.highlight_code import highlight_code + + +FAKE_PLAIN_TEXT_NAME = 'fake_file.barbaz' +FAKE_PLAIN_TEXT_CONTENT = """\ +This is a fake text content for testing purposes. +""" + +FAKE_PYTHON_NAME = 'fake_file.py' +FAKE_PYTHON_CONTENT = """\ +def fake_function(): + print("This is a fake Python function.") +""" +FAKE_PYTHON_RESULT = """\ +
1def fake_function():\n2    print("This is a fake Python function.")\n
+""" + + +class HighlightCodeTestCase(TestCase): + def test_python_highlighting(self): + # Test that Python code gets highlighted with pygments classes + result = highlight_code(FAKE_PYTHON_CONTENT, FAKE_PYTHON_NAME) + self.assertTrue(result.startswith('
') and result.endswith('
\n')) + self.assertTrue(FAKE_PYTHON_RESULT == result) + + def test_unknown_extension_fallback(self): + result = highlight_code(FAKE_PLAIN_TEXT_CONTENT, FAKE_PLAIN_TEXT_NAME) + self.assertTrue(result.startswith('
') and result.endswith('
')) + self.assertTrue(FAKE_PLAIN_TEXT_CONTENT in result) + + def test_empty_content(self): + result = highlight_code('', 'FAKE_PLAIN_TEXT_NAME') + self.assertTrue(result.startswith('
'))
+        self.assertTrue(len(result) == 11)
+
+        result = highlight_code(None, 'FAKE_PLAIN_TEXT_NAME')
+        self.assertTrue(result.startswith('
'))
+        self.assertTrue(len(result) == 11)
+
+    def test_empty_filename(self):
+        result = highlight_code(' ', '')
+        self.assertTrue(result.startswith('
 
')) + self.assertTrue(len(result) == 12) + + result = highlight_code(' ', None) + self.assertTrue(result.startswith('
 
')) + self.assertTrue(len(result) == 12) diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index ab11dafff1d8ac6bd5a31fd56b72d66ea3bb4679..e0937e9fe039d3c0c9e4d35b780e1851f4d65002 100644 GIT binary patch delta 5202 zcmcImOOG2x5LO=SS_uK6-AGw)5Qe=#vbOwKd+pr__zPT;3zvC&;+dY2+i|=?NICe9 zP$c{x;0hN`@DKPYNJyN)T|HGb-HkU$aP6+gS6@|ie?R>3;cq{E^lE)$eRF+leS7`Z zdT+h|W8?d)jsLuH^yMP#WT}WmAzK|$%yTdMDk!pIesMA!kD~M8rSAo^WL~@wVOw8x z&j+Kk{$=TfVJeb#7R8H;VY$MCd74bs$;Nx$XijD6i7Ll`7yB~z=1D5rkgV4&S1Q{9 zuSb?qkb2qX^Jk6b2OLI6-&AnA-R|c1>HguzaU2I-EM#|O$2X?m{jztse*%1L(;q)7 zuaZ0x3)v2;*qX+;5R0~4TxHQky700zQ1%0~kLL5rV?3CgO-2sl!(h^iOoGy(KX0J3g>>Fyx`NW#j2b&3i~qUu_A*d6Z) zB2FSiz&&0@YQfIC-RlwIP%%WGP=cuo5ye*wvHDUpr`ciGI1a4AEE%?^KN|aCAHW;Q znE5re8{JJ_AU0T^*RTF;ZlAa&1GuPc0D&<8Ggu_DpJ6}yp4Xd<4LWAH9D#r4_eN&8 zQq2ko-le{wo!`P+8&5dP4_MFg#S24kY(Jl&KMPmm*4Lc}Ii_kns z)d3+Co6nv#9d-ql`N28XTJnIFl1j|?XGH}5|X`GR`x7DT^BE_Yt}EqUz(JY zIlYx+o|~NJX8k$5fXoFbWCtee(la=eW)}TBP*ov*@XpDQIA-ikCj_!@bX6K%m1YqV ziXZ@TRnh7OfLr3+DhaPN3+I}20_?*U(m@Puql2Y6+snitb!nEc#IMOYIH`l;!_7B8 z-`;DrxIU@tt;xXad7Jlsy?OBHB#NSOIDA7P0->8Nu5c=fDd>|o59-!dkmi1^0?;BZoYWhxUah3WPT1A2Ep$!qs$78FXE2S_zeRSL>CXevO?qcY=nM?0Sczz zc1pztxC_@4f6PN+K2mGIz{8pqiZ-*F8m^v=3*Uu*hs!1609TvgA}yz<#;N7N@6(ze z!s3pkf_BX0l9U~!d(6|Jz;kD#*{H>K=sbwcCpf`5GX|yx0J{t?rAzcVU|&NL z!RE6<5>OXgNx%cOeYFZvYtaJ^F!)r>^lCS0+Fg(`GIWG5VQ6`H>?%nPM|ea} zN)fDhe@HMI{o&6*4iJv0LZqI4XFE3OM*j|LLx!sH0CM(y^J8MsF z6QA21lb%Jqv@=#;C8NfrMr~?Hl delta 27 icmcbxRcZBS#fBEf7N!>F7M2#)7Pc1lEgY*V8My$PEeVML diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index 6dbd34846..d2350df93 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -28,3 +28,7 @@ @import 'custom/misc'; @import 'custom/notifications'; @import 'custom/racks'; + +// Pygments styles +@import 'pygments/dark'; +@import 'pygments/light'; diff --git a/netbox/project-static/styles/pygments/README.md b/netbox/project-static/styles/pygments/README.md new file mode 100644 index 000000000..cc213892e --- /dev/null +++ b/netbox/project-static/styles/pygments/README.md @@ -0,0 +1,33 @@ +## Pygments style for NetBox + +The style are based on the `Pygments` themes `solarized-light` and `solarized-dark`. +To generated the scss files in this directory execute the following code in a Python environment where `Pygments` is installed: + +```python +from pygments.formatters import HtmlFormatter +h = HtmlFormatter(linenos="inline", classprefix="pygments-", style="solarized-dark") +print(h.get_style_defs()) + +h = HtmlFormatter(linenos="inline", classprefix="pygments-", style="solarized-light") +print(h.get_style_defs()) +``` + +To get the correct theme for dark and light modes wrap the resulting CSS in the following selectors: + +```scss +// _dark.scss +body[data-bs-theme='dark'] { + // Insert generated CSS for dark theme here +} + +// _light.scss +body[data-bs-theme='light'] { + // Insert generated CSS for light theme here +} +``` + +The run the formatter: + +```bash +yarn run format:styles +``` \ No newline at end of file diff --git a/netbox/project-static/styles/pygments/_dark.scss b/netbox/project-static/styles/pygments/_dark.scss new file mode 100644 index 000000000..610d86f0d --- /dev/null +++ b/netbox/project-static/styles/pygments/_dark.scss @@ -0,0 +1,277 @@ +body[data-bs-theme='dark'] { + td.linenos .normal { + color: #586e75; + background-color: #073642; + padding-left: 5px; + padding-right: 5px; + } + span.linenos { + color: #586e75; + background-color: #073642; + padding-left: 5px; + padding-right: 5px; + margin-right: 10px; + } + td.linenos .special { + color: #000000; + background-color: #ffffc0; + padding-left: 5px; + padding-right: 5px; + } + span.linenos.special { + color: #000000; + background-color: #ffffc0; + padding-left: 5px; + padding-right: 5px; + } + .hll { + background-color: #073642; + } + .pygments-c { + color: #586e75; + font-style: italic; + } /* Comment */ + .pygments-err { + color: #839496; + background-color: #dc322f; + } /* Error */ + .pygments-esc { + color: #839496; + } /* Escape */ + .pygments-g { + color: #839496; + } /* Generic */ + .pygments-k { + color: #859900; + } /* Keyword */ + .pygments-l { + color: #839496; + } /* Literal */ + .pygments-n { + color: #839496; + } /* Name */ + .pygments-o { + color: #586e75; + } /* Operator */ + .pygments-x { + color: #839496; + } /* Other */ + .pygments-p { + color: #839496; + } /* Punctuation */ + .pygments-ch { + color: #586e75; + font-style: italic; + } /* Comment.Hashbang */ + .pygments-cm { + color: #586e75; + font-style: italic; + } /* Comment.Multiline */ + .pygments-cp { + color: #d33682; + } /* Comment.Preproc */ + .pygments-cpf { + color: #586e75; + } /* Comment.PreprocFile */ + .pygments-c1 { + color: #586e75; + font-style: italic; + } /* Comment.Single */ + .pygments-cs { + color: #586e75; + font-style: italic; + } /* Comment.Special */ + .pygments-gd { + color: #dc322f; + } /* Generic.Deleted */ + .pygments-ge { + color: #839496; + font-style: italic; + } /* Generic.Emph */ + .pygments-ges { + color: #839496; + font-weight: bold; + font-style: italic; + } /* Generic.EmphStrong */ + .pygments-gr { + color: #dc322f; + } /* Generic.Error */ + .pygments-gh { + color: #839496; + font-weight: bold; + } /* Generic.Heading */ + .pygments-gi { + color: #859900; + } /* Generic.Inserted */ + .pygments-go { + color: #839496; + } /* Generic.Output */ + .pygments-gp { + color: #268bd2; + font-weight: bold; + } /* Generic.Prompt */ + .pygments-gs { + color: #839496; + font-weight: bold; + } /* Generic.Strong */ + .pygments-gu { + color: #839496; + text-decoration: underline; + } /* Generic.Subheading */ + .pygments-gt { + color: #268bd2; + } /* Generic.Traceback */ + .pygments-kc { + color: #2aa198; + } /* Keyword.Constant */ + .pygments-kd { + color: #2aa198; + } /* Keyword.Declaration */ + .pygments-kn { + color: #cb4b16; + } /* Keyword.Namespace */ + .pygments-kp { + color: #859900; + } /* Keyword.Pseudo */ + .pygments-kr { + color: #859900; + } /* Keyword.Reserved */ + .pygments-kt { + color: #b58900; + } /* Keyword.Type */ + .pygments-ld { + color: #839496; + } /* Literal.Date */ + .pygments-m { + color: #2aa198; + } /* Literal.Number */ + .pygments-s { + color: #2aa198; + } /* Literal.String */ + .pygments-na { + color: #839496; + } /* Name.Attribute */ + .pygments-nb { + color: #268bd2; + } /* Name.Builtin */ + .pygments-nc { + color: #268bd2; + } /* Name.Class */ + .pygments-no { + color: #268bd2; + } /* Name.Constant */ + .pygments-nd { + color: #268bd2; + } /* Name.Decorator */ + .pygments-ni { + color: #268bd2; + } /* Name.Entity */ + .pygments-ne { + color: #268bd2; + } /* Name.Exception */ + .pygments-nf { + color: #268bd2; + } /* Name.Function */ + .pygments-nl { + color: #268bd2; + } /* Name.Label */ + .pygments-nn { + color: #268bd2; + } /* Name.Namespace */ + .pygments-nx { + color: #839496; + } /* Name.Other */ + .pygments-py { + color: #839496; + } /* Name.Property */ + .pygments-nt { + color: #268bd2; + } /* Name.Tag */ + .pygments-nv { + color: #268bd2; + } /* Name.Variable */ + .pygments-ow { + color: #859900; + } /* Operator.Word */ + .pygments-pm { + color: #839496; + } /* Punctuation.Marker */ + .pygments-w { + color: #839496; + } /* Text.Whitespace */ + .pygments-mb { + color: #2aa198; + } /* Literal.Number.Bin */ + .pygments-mf { + color: #2aa198; + } /* Literal.Number.Float */ + .pygments-mh { + color: #2aa198; + } /* Literal.Number.Hex */ + .pygments-mi { + color: #2aa198; + } /* Literal.Number.Integer */ + .pygments-mo { + color: #2aa198; + } /* Literal.Number.Oct */ + .pygments-sa { + color: #2aa198; + } /* Literal.String.Affix */ + .pygments-sb { + color: #2aa198; + } /* Literal.String.Backtick */ + .pygments-sc { + color: #2aa198; + } /* Literal.String.Char */ + .pygments-dl { + color: #2aa198; + } /* Literal.String.Delimiter */ + .pygments-sd { + color: #586e75; + } /* Literal.String.Doc */ + .pygments-s2 { + color: #2aa198; + } /* Literal.String.Double */ + .pygments-se { + color: #2aa198; + } /* Literal.String.Escape */ + .pygments-sh { + color: #2aa198; + } /* Literal.String.Heredoc */ + .pygments-si { + color: #2aa198; + } /* Literal.String.Interpol */ + .pygments-sx { + color: #2aa198; + } /* Literal.String.Other */ + .pygments-sr { + color: #cb4b16; + } /* Literal.String.Regex */ + .pygments-s1 { + color: #2aa198; + } /* Literal.String.Single */ + .pygments-ss { + color: #2aa198; + } /* Literal.String.Symbol */ + .pygments-bp { + color: #268bd2; + } /* Name.Builtin.Pseudo */ + .pygments-fm { + color: #268bd2; + } /* Name.Function.Magic */ + .pygments-vc { + color: #268bd2; + } /* Name.Variable.Class */ + .pygments-vg { + color: #268bd2; + } /* Name.Variable.Global */ + .pygments-vi { + color: #268bd2; + } /* Name.Variable.Instance */ + .pygments-vm { + color: #268bd2; + } /* Name.Variable.Magic */ + .pygments-il { + color: #2aa198; + } /* Literal.Number.Integer.Long */ +} diff --git a/netbox/project-static/styles/pygments/_light.scss b/netbox/project-static/styles/pygments/_light.scss new file mode 100644 index 000000000..d7d540983 --- /dev/null +++ b/netbox/project-static/styles/pygments/_light.scss @@ -0,0 +1,277 @@ +body[data-bs-theme='light'] { + td.linenos .normal { + color: #93a1a1; + background-color: #eee8d5; + padding-left: 5px; + padding-right: 5px; + } + span.linenos { + color: #93a1a1; + background-color: #eee8d5; + padding-left: 5px; + padding-right: 5px; + margin-right: 10px; + } + td.linenos .special { + color: #000000; + background-color: #ffffc0; + padding-left: 5px; + padding-right: 5px; + } + span.linenos.special { + color: #000000; + background-color: #ffffc0; + padding-left: 5px; + padding-right: 5px; + } + .hll { + background-color: #eee8d5; + } + .pygments-c { + color: #93a1a1; + font-style: italic; + } /* Comment */ + .pygments-err { + color: #657b83; + background-color: #dc322f; + } /* Error */ + .pygments-esc { + color: #657b83; + } /* Escape */ + .pygments-g { + color: #657b83; + } /* Generic */ + .pygments-k { + color: #859900; + } /* Keyword */ + .pygments-l { + color: #657b83; + } /* Literal */ + .pygments-n { + color: #657b83; + } /* Name */ + .pygments-o { + color: #93a1a1; + } /* Operator */ + .pygments-x { + color: #657b83; + } /* Other */ + .pygments-p { + color: #657b83; + } /* Punctuation */ + .pygments-ch { + color: #93a1a1; + font-style: italic; + } /* Comment.Hashbang */ + .pygments-cm { + color: #93a1a1; + font-style: italic; + } /* Comment.Multiline */ + .pygments-cp { + color: #d33682; + } /* Comment.Preproc */ + .pygments-cpf { + color: #93a1a1; + } /* Comment.PreprocFile */ + .pygments-c1 { + color: #93a1a1; + font-style: italic; + } /* Comment.Single */ + .pygments-cs { + color: #93a1a1; + font-style: italic; + } /* Comment.Special */ + .pygments-gd { + color: #dc322f; + } /* Generic.Deleted */ + .pygments-ge { + color: #657b83; + font-style: italic; + } /* Generic.Emph */ + .pygments-ges { + color: #657b83; + font-weight: bold; + font-style: italic; + } /* Generic.EmphStrong */ + .pygments-gr { + color: #dc322f; + } /* Generic.Error */ + .pygments-gh { + color: #657b83; + font-weight: bold; + } /* Generic.Heading */ + .pygments-gi { + color: #859900; + } /* Generic.Inserted */ + .pygments-go { + color: #657b83; + } /* Generic.Output */ + .pygments-gp { + color: #268bd2; + font-weight: bold; + } /* Generic.Prompt */ + .pygments-gs { + color: #657b83; + font-weight: bold; + } /* Generic.Strong */ + .pygments-gu { + color: #657b83; + text-decoration: underline; + } /* Generic.Subheading */ + .pygments-gt { + color: #268bd2; + } /* Generic.Traceback */ + .pygments-kc { + color: #2aa198; + } /* Keyword.Constant */ + .pygments-kd { + color: #2aa198; + } /* Keyword.Declaration */ + .pygments-kn { + color: #cb4b16; + } /* Keyword.Namespace */ + .pygments-kp { + color: #859900; + } /* Keyword.Pseudo */ + .pygments-kr { + color: #859900; + } /* Keyword.Reserved */ + .pygments-kt { + color: #b58900; + } /* Keyword.Type */ + .pygments-ld { + color: #657b83; + } /* Literal.Date */ + .pygments-m { + color: #2aa198; + } /* Literal.Number */ + .pygments-s { + color: #2aa198; + } /* Literal.String */ + .pygments-na { + color: #657b83; + } /* Name.Attribute */ + .pygments-nb { + color: #268bd2; + } /* Name.Builtin */ + .pygments-nc { + color: #268bd2; + } /* Name.Class */ + .pygments-no { + color: #268bd2; + } /* Name.Constant */ + .pygments-nd { + color: #268bd2; + } /* Name.Decorator */ + .pygments-ni { + color: #268bd2; + } /* Name.Entity */ + .pygments-ne { + color: #268bd2; + } /* Name.Exception */ + .pygments-nf { + color: #268bd2; + } /* Name.Function */ + .pygments-nl { + color: #268bd2; + } /* Name.Label */ + .pygments-nn { + color: #268bd2; + } /* Name.Namespace */ + .pygments-nx { + color: #657b83; + } /* Name.Other */ + .pygments-py { + color: #657b83; + } /* Name.Property */ + .pygments-nt { + color: #268bd2; + } /* Name.Tag */ + .pygments-nv { + color: #268bd2; + } /* Name.Variable */ + .pygments-ow { + color: #859900; + } /* Operator.Word */ + .pygments-pm { + color: #657b83; + } /* Punctuation.Marker */ + .pygments-w { + color: #657b83; + } /* Text.Whitespace */ + .pygments-mb { + color: #2aa198; + } /* Literal.Number.Bin */ + .pygments-mf { + color: #2aa198; + } /* Literal.Number.Float */ + .pygments-mh { + color: #2aa198; + } /* Literal.Number.Hex */ + .pygments-mi { + color: #2aa198; + } /* Literal.Number.Integer */ + .pygments-mo { + color: #2aa198; + } /* Literal.Number.Oct */ + .pygments-sa { + color: #2aa198; + } /* Literal.String.Affix */ + .pygments-sb { + color: #2aa198; + } /* Literal.String.Backtick */ + .pygments-sc { + color: #2aa198; + } /* Literal.String.Char */ + .pygments-dl { + color: #2aa198; + } /* Literal.String.Delimiter */ + .pygments-sd { + color: #93a1a1; + } /* Literal.String.Doc */ + .pygments-s2 { + color: #2aa198; + } /* Literal.String.Double */ + .pygments-se { + color: #2aa198; + } /* Literal.String.Escape */ + .pygments-sh { + color: #2aa198; + } /* Literal.String.Heredoc */ + .pygments-si { + color: #2aa198; + } /* Literal.String.Interpol */ + .pygments-sx { + color: #2aa198; + } /* Literal.String.Other */ + .pygments-sr { + color: #cb4b16; + } /* Literal.String.Regex */ + .pygments-s1 { + color: #2aa198; + } /* Literal.String.Single */ + .pygments-ss { + color: #2aa198; + } /* Literal.String.Symbol */ + .pygments-bp { + color: #268bd2; + } /* Name.Builtin.Pseudo */ + .pygments-fm { + color: #268bd2; + } /* Name.Function.Magic */ + .pygments-vc { + color: #268bd2; + } /* Name.Variable.Class */ + .pygments-vg { + color: #268bd2; + } /* Name.Variable.Global */ + .pygments-vi { + color: #268bd2; + } /* Name.Variable.Instance */ + .pygments-vm { + color: #268bd2; + } /* Name.Variable.Magic */ + .pygments-il { + color: #2aa198; + } /* Literal.Number.Integer.Long */ +} diff --git a/netbox/templates/core/datafile.html b/netbox/templates/core/datafile.html index 175a0e2bc..efe6566e9 100644 --- a/netbox/templates/core/datafile.html +++ b/netbox/templates/core/datafile.html @@ -4,6 +4,7 @@ {% load helpers %} {% load perms %} {% load plugins %} +{% load highlight_code %} {% load i18n %} {% block breadcrumbs %} @@ -54,7 +55,7 @@

{% trans "Content" %}

-
{{ object.data_as_string }}
+ {% highlight_code object.data_as_string object.path %}
{% plugin_left_page object %} diff --git a/requirements.txt b/requirements.txt index 3a6128f04..9b57ec0ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,6 +28,7 @@ netaddr==1.3.0 nh3==0.3.0 Pillow==11.3.0 psycopg[c,pool]==3.2.9 +pygments==2.19.2 PyYAML==6.0.2 requests==2.32.4 rq==2.4.1