about summary refs log tree commit diff
diff options
context:
space:
mode:
authorNguyễn Gia Phong <cnx@loang.net>2025-05-28 20:34:25 +0900
committerNguyễn Gia Phong <cnx@loang.net>2025-05-28 20:34:25 +0900
commit42b2fb1d329f614822d3c5b080185199e4be98e2 (patch)
tree007fd02a8c29477b90883ff65c760e764c9e705a
parent37e9fc1b09112d59ec9d4d38f0ab449979f7e5c0 (diff)
downloadscadere-42b2fb1d329f614822d3c5b080185199e4be98e2.tar.gz
Unuse subcommand for help2man
-rw-r--r--pyproject.toml10
-rw-r--r--src/scadere/__init__.py64
-rw-r--r--src/scadere/__main__.py111
-rw-r--r--src/scadere/check.py42
-rw-r--r--src/scadere/listen.py39
-rw-r--r--tst/test_check.py8
-rw-r--r--tst/test_listen.py8
7 files changed, 150 insertions, 132 deletions
diff --git a/pyproject.toml b/pyproject.toml
index d11b2ba..3e2d251 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,8 +21,10 @@ classifiers = [
     'Programming Language :: Python',
     'Topic :: Utilities' ]
 dynamic = [ 'version' ]
-urls = { Source = 'https://trong.loang.net/scadere' }
-scripts = { scadere = 'scadere.__main__:main' }
+
+[project.urls]
+Source = 'https://trong.loang.net/scadere'
+Inbox = 'https://loa.loang.net/chung'
 
 [project.optional-dependencies]
 dev = [ 'coverage',
@@ -32,6 +34,10 @@ dev = [ 'coverage',
         'pytest-asyncio',
         'trustme >= 1.2.0' ]
 
+[project.scripts]
+scadere-check = 'scadere.check:main'
+scadere-listen = 'scadere.listen:main'
+
 [tool.pytest.ini_options]
 asyncio_mode = 'auto'
 asyncio_default_fixture_loop_scope = 'function'
diff --git a/src/scadere/__init__.py b/src/scadere/__init__.py
index 2a73e5d..7becab1 100644
--- a/src/scadere/__init__.py
+++ b/src/scadere/__init__.py
@@ -1,2 +1,62 @@
-__all__ = ['__version__']
-__version__ = '0.0.1'
+# Root package, with some helpers
+# Copyright (C) 2025  Nguyễn Gia Phong
+#
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Scadere is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with scadere.  If not, see <https://www.gnu.org/licenses/>.
+
+from argparse import HelpFormatter
+
+__all__ = ['__version__', 'GNUHelpFormatter', 'NetLoc']
+__version__ = '0.1.0'
+
+
+class GNUHelpFormatter(HelpFormatter):
+    """Help formatter for ArgumentParser following GNU Coding Standards."""
+
+    def add_usage(self, usage, actions, groups, prefix='Usage: '):
+        """Substitute 'Usage:' for 'usage:'."""
+        super().add_usage(usage, actions, groups, prefix)
+
+    def start_section(self, heading):
+        """Substitute 'Options:' for 'options:'."""
+        super().start_section(heading.capitalize())
+
+    def _format_action_invocation(self, action):
+        """Format --long-option=argument."""
+        if not action.option_strings or action.nargs is not None:
+            return super()._format_action_invocation(action)
+        arg = self._format_args(action,
+                                self._get_default_metavar_for_optional(action))
+        return ', '.join(f"{opt}{'=' if opt.startswith('--') else ' '}{arg}"
+                         for opt in action.option_strings)
+
+    def add_argument(self, action):
+        """Suppress positional arguments."""
+        if action.option_strings:
+            super().add_argument(action)
+
+
+class NetLoc:
+    """Network location with default port."""
+
+    def __init__(self, default_port):
+        self.default_port = default_port
+
+    def __call__(self, string):
+        """Return hostname and port from given netloc."""
+        if ':' not in string:
+            return string, self.default_port
+        hostname, port = string.rsplit(':', 1)
+        return hostname, int(port)  # ValueError to be handled by argparse
diff --git a/src/scadere/__main__.py b/src/scadere/__main__.py
deleted file mode 100644
index 3467f95..0000000
--- a/src/scadere/__main__.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# Executable entry point
-# Copyright (C) 2025  Nguyễn Gia Phong
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-from argparse import ArgumentParser, FileType, HelpFormatter
-from asyncio import run
-from datetime import datetime, timedelta, timezone
-from pathlib import Path
-from sys import stdout
-
-from . import __version__
-from .check import check
-from .listen import listen
-
-
-class GNUHelpFormatter(HelpFormatter):
-    """Help formatter for ArgumentParser following GNU Coding Standards."""
-
-    def add_usage(self, usage, actions, groups, prefix='Usage: '):
-        """Substitute 'Usage:' for 'usage:'."""
-        super().add_usage(usage, actions, groups, prefix)
-
-    def start_section(self, heading):
-        """Substitute 'Options:' for 'options:'."""
-        super().start_section(heading.capitalize())
-
-    def _format_action_invocation(self, action):
-        """Format --long-option=argument."""
-        if not action.option_strings or action.nargs is not None:
-            return super()._format_action_invocation(action)
-        arg = self._format_args(action,
-                                self._get_default_metavar_for_optional(action))
-        return ', '.join(f"{opt}{'=' if opt.startswith('--') else ' '}{arg}"
-                         for opt in action.option_strings)
-
-    def add_argument(self, action):
-        """Suppress positional arguments."""
-        if action.option_strings:
-            super().add_argument(action)
-
-
-class NetLoc:
-    def __init__(self, default_port):
-        self.default_port = default_port
-
-    def __call__(self, string):
-        """Return hostname and port from given netloc."""
-        if ':' not in string:
-            return string, self.default_port
-        hostname, port = string.rsplit(':', 1)
-        return hostname, int(port)  # ValueError to be handled by argparse
-
-
-def main():
-    """Parse arguments and launch subprogram."""
-    parser = ArgumentParser(prog='scadere', allow_abbrev=False,
-                            formatter_class=GNUHelpFormatter)
-    parser.add_argument('-v', '--version', action='version',
-                        version=f'%(prog)s {__version__}')
-    subparsers = parser.add_subparsers(help='subcommand help', required=True)
-
-    check_description = ('Check TLS certificate expiration of HOST,'
-                         ' where PORT defaults to 443.')
-    check_parser = subparsers.add_parser('check',
-                                         description=check_description,
-                                         formatter_class=GNUHelpFormatter)
-    check_parser.set_defaults(subcommand='check')
-    check_parser.add_argument('netloc', metavar='HOST[:PORT]',
-                              nargs='+', type=NetLoc(443))
-    check_parser.add_argument('-d', '--days', type=float, default=7,
-                              help='days before expiration (default to 7)')
-    check_parser.add_argument('-o', '--output', metavar='PATH',
-                              type=FileType('w'), default=stdout,
-                              help='output file (default to stdout)')
-
-    listen_description = ('Serve the TLS certificate expiration feed'
-                          ' from INPUT file for base URL at HOST:PORT,'
-                          ' where HOST defaults to localhost and PORT'
-                          ' is selected randomly if not specified.')
-    listen_parser = subparsers.add_parser('listen',
-                                          description=listen_description,
-                                          formatter_class=GNUHelpFormatter)
-    listen_parser.add_argument('certs', metavar='INPUT', type=Path)
-    listen_parser.add_argument('base_url', metavar='URL')
-    listen_parser.add_argument('netloc', metavar='[HOST][:PORT]', nargs='?',
-                               type=NetLoc(None), default=('localhost', None))
-    listen_parser.set_defaults(subcommand='listen')
-
-    args = parser.parse_args()
-    if args.subcommand == 'check':
-        with args.output:
-            after = datetime.now(tz=timezone.utc) + timedelta(days=args.days)
-            check(args.netloc, after, args.output)
-    elif args.subcommand == 'listen':
-        run(listen(args.certs, args.base_url, *args.netloc))
-
-
-if __name__ == '__main__':
-    main()
diff --git a/src/scadere/check.py b/src/scadere/check.py
index fec0b22..7ef2d4a 100644
--- a/src/scadere/check.py
+++ b/src/scadere/check.py
@@ -1,27 +1,33 @@
 # TLS certificate expiration checker
 # Copyright (C) 2025  Nguyễn Gia Phong
 #
-# This program is free software: you can redistribute it and/or modify
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as published
 # by the Free Software Foundation, either version 3 of the License, or
 # (at your option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
+# Scadere is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+# along with scadere.  If not, see <https://www.gnu.org/licenses/>.
 
+from argparse import ArgumentParser, FileType
 from base64 import urlsafe_b64encode as base64
+from datetime import datetime, timedelta, timezone
 from email.utils import parsedate_to_datetime as parsedate
 from itertools import chain
 from socket import AF_INET, socket
 from ssl import create_default_context as tls_context
-from sys import stderr
+from sys import stderr, stdout
+
+from . import __version__, GNUHelpFormatter, NetLoc
 
-__all__ = ['check']
+__all__ = ['check', 'main']
 
 
 def check(netlocs, after, output, fake_ca=None):
@@ -58,3 +64,29 @@ def check(netlocs, after, output, fake_ca=None):
                       hostname, port, cert['serialNumber'],
                       base64(ca.encode()).decode(),
                       file=output)
+
+
+def main():
+    """Run TLS checker."""
+    description = ('Check TLS certificate expiration of HOST,'
+                   ' where PORT defaults to 443.')
+    parser = ArgumentParser(prog='scadere-check', allow_abbrev=False,
+                            description=description,
+                            formatter_class=GNUHelpFormatter)
+    parser.add_argument('-v', '--version', action='version',
+                        version=f'%(prog)s {__version__}')
+    parser.add_argument('netloc', metavar='HOST[:PORT]',
+                        nargs='+', type=NetLoc(443))
+    parser.add_argument('-d', '--days', type=float, default=7,
+                        help='days before expiration (default to 7)')
+    parser.add_argument('-o', '--output', metavar='PATH',
+                        type=FileType('w'), default=stdout,
+                        help='output file (default to stdout)')
+    args = parser.parse_args()
+    with args.output:
+        after = datetime.now(tz=timezone.utc) + timedelta(days=args.days)
+        check(args.netloc, after, args.output)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/src/scadere/listen.py b/src/scadere/listen.py
index 1cf822a..4870e2f 100644
--- a/src/scadere/listen.py
+++ b/src/scadere/listen.py
@@ -1,31 +1,35 @@
 # HTTP server for Atom feed of TLS certificate expirations
 # Copyright (C) 2025  Nguyễn Gia Phong
 #
-# This program is free software: you can redistribute it and/or modify
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as published
 # by the Free Software Foundation, either version 3 of the License, or
 # (at your option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
+# Scadere is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+# along with scadere.  If not, see <https://www.gnu.org/licenses/>.
 
-from asyncio import start_server
+from argparse import ArgumentParser
+from asyncio import run, start_server
 from base64 import urlsafe_b64decode as from_base64
 from datetime import datetime
 from functools import partial
+from pathlib import Path
 from urllib.parse import parse_qs, urljoin, urlsplit
 from xml.etree.ElementTree import (Element as xml_element,
                                    SubElement as xml_subelement,
                                    indent, tostring as str_from_xml)
 
-from . import __version__
+from . import __version__, GNUHelpFormatter, NetLoc
 
-__all__ = ['listen']
+__all__ = ['listen', 'main', 'parse_summary']
 
 
 def parse_summary(line):
@@ -162,3 +166,26 @@ async def listen(certs, base_url, host, port):  # pragma: no cover
         print('Serving on', end=' ')
         print(*(socket.getsockname() for socket in server.sockets), sep=', ')
         await server.serve_forever()
+
+
+def main():
+    """Launch server."""
+    description = ('Serve the TLS certificate expiration feed'
+                   ' from INPUT file for base URL at HOST:PORT,'
+                   ' where HOST defaults to localhost and PORT'
+                   ' is selected randomly if not specified.')
+    parser = ArgumentParser(prog='scadere-listen', allow_abbrev=False,
+                            description=description,
+                            formatter_class=GNUHelpFormatter)
+    parser.add_argument('-v', '--version', action='version',
+                        version=f'%(prog)s {__version__}')
+    parser.add_argument('certs', metavar='INPUT', type=Path)
+    parser.add_argument('base_url', metavar='URL')
+    parser.add_argument('netloc', metavar='[HOST][:PORT]', nargs='?',
+                        type=NetLoc(None), default=('localhost', None))
+    args = parser.parse_args()
+    run(listen(args.certs, args.base_url, *args.netloc))
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tst/test_check.py b/tst/test_check.py
index bfbe927..b9a89ff 100644
--- a/tst/test_check.py
+++ b/tst/test_check.py
@@ -1,18 +1,20 @@
 # Tests for the TLS client
 # Copyright (C) 2025  Nguyễn Gia Phong
 #
-# This program is free n redistribute it and/or modify
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as published
 # by the Free Software Foundation, either version 3 of the License, or
 # (at your option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
+# Scadere is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+# along with scadere.  If not, see <https://www.gnu.org/licenses/>.
 
 from asyncio import get_running_loop, start_server
 from base64 import urlsafe_b64encode as base64
diff --git a/tst/test_listen.py b/tst/test_listen.py
index 1409834..ec98140 100644
--- a/tst/test_listen.py
+++ b/tst/test_listen.py
@@ -1,18 +1,20 @@
 # Tests for the HTTP server
 # Copyright (C) 2025  Nguyễn Gia Phong
 #
-# This program is free software: you can redistribute it and/or modify
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as published
 # by the Free Software Foundation, either version 3 of the License, or
 # (at your option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
+# Scadere is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+# along with scadere.  If not, see <https://www.gnu.org/licenses/>.
 
 from asyncio import open_connection, start_server
 from base64 import (urlsafe_b64decode as from_base64,