summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--ganarchy/cli/debug.py10
-rw-r--r--ganarchy/data.py177
-rw-r--r--requirements.txt1
3 files changed, 104 insertions, 84 deletions
diff --git a/ganarchy/cli/debug.py b/ganarchy/cli/debug.py
index 95ad045..e44d235 100644
--- a/ganarchy/cli/debug.py
+++ b/ganarchy/cli/debug.py
@@ -15,11 +15,11 @@
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 import click
-import qtoml
 
 import ganarchy
 import ganarchy.cli
 import ganarchy.data
+import ganarchy.dirs
 
 @ganarchy.cli.main.group()
 def debug():
@@ -27,10 +27,10 @@ def debug():
 
 @debug.command()
 def paths():
-    click.echo('Config home: {}'.format(ganarchy.config_home))
-    click.echo('Additional config search path: {}'.format(ganarchy.config_dirs))
-    click.echo('Cache home: {}'.format(ganarchy.cache_home))
-    click.echo('Data home: {}'.format(ganarchy.data_home))
+    click.echo('Config home: {}'.format(ganarchy.dirs.CONFIG_HOME))
+    click.echo('Additional config search path: {}'.format(ganarchy.dirs.CONFIG_DIRS))
+    click.echo('Cache home: {}'.format(ganarchy.dirs.CACHE_HOME))
+    click.echo('Data home: {}'.format(ganarchy.dirs.DATA_HOME))
 
 def print_data_source(data_source):
     if ganarchy.data.DataProperty.INSTANCE_TITLE in data_source.get_supported_properties():
diff --git a/ganarchy/data.py b/ganarchy/data.py
index a69186f..34d5a2a 100644
--- a/ganarchy/data.py
+++ b/ganarchy/data.py
@@ -1,5 +1,5 @@
 # This file is part of GAnarchy - decentralized project hub
-# Copyright (C) 2019, 2020  Soni L.
+# Copyright (C) 2019, 2020, 2024  Soni L.
 #
 # 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
@@ -26,10 +26,8 @@ import itertools
 import os
 import re
 import time
+import tomllib
 
-import abdl
-import abdl.exceptions
-import qtoml
 import requests
 
 from enum import Enum
@@ -38,64 +36,47 @@ from urllib.parse import urlparse
 import ganarchy.dirs
 
 # TODO move elsewhere
-class URIPredicate(abdl.predicates.Predicate):
-    def __init__(self, ports=range(1,65536), schemes=('https',)):
-        self.ports = ports
-        self.schemes = schemes
+class _ValidationError(Exception):
+    # we have no idea how classes work in python anymore, it's been 2 years
+    pass
 
-    def accept(self, obj):
-        try:
-            u = urlparse(obj)
-            if not u:
-                return False
-            # also raises for invalid ports, see https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlparse
-            # "Reading the port attribute will raise a ValueError if an invalid port is specified in the URL. [...]"
-            if u.port is not None and u.port not in self.ports:
-                return False
-            if u.scheme not in self.schemes:
-                return False
-        except ValueError:
+def _check_type(obj, ty):
+    if isinstance(obj, ty):
+        return obj
+    raise _ValidationError # TODO...
+
+def _is_uri(obj, ports=range(1,65536), schemes=('https',)):
+    try:
+        u = urlparse(obj)
+        if not u:
             return False
-        return True
+        # also raises for invalid ports, see
+        # https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlparse
+        # "Reading the port attribute will raise a ValueError if an
+        # invalid port is specified in the URL. [...]"
+        if u.port is not None and u.port not in ports:
+            return False
+        if u.scheme not in schemes:
+            return False
+    except ValueError:
+        return False
+    return True
+
+def _check_uri(obj, ports=range(1,65536), schemes=('https',)):
+    _check_type(obj, str)
+    if _is_uri(obj, ports, schemes):
+        return obj
+    raise _ValidationError # TODO...
+
+_commit_pattern = re.compile(r"^[0-9a-fA-F]{40}$")
+_commit_sha256_pattern = re.compile(r"^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$")
+
+def _is_commit(obj, sha256ready=True):
+    if sha256ready:
+        return _commit_sha256_pattern.match(obj)
+    else:
+        return _commit_pattern.match(obj)
 
-class CommitPredicate(abdl.predicates.Predicate):
-    def __init__(self, sha256ready=True):
-        if sha256ready:
-            self.re = re.compile(r"^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$")
-        else:
-            self.re = re.compile(r"^[0-9a-fA-F]{40}$")
-
-    def accept(self, obj):
-        return self.re.match(obj)
-
-# sanitize = skip invalid entries
-# validate = error on invalid entries
-# LEGACY. DO NOT USE.
-# TODO remove
-CONFIG_REPOS_SANITIZE = abdl.compile("""->'projects'?:?$dict
-                                          ->commit[:?$str:?$commit]:?$dict
-                                            ->url[:?$str:?$uri]:?$dict
-                                              ->branch:?$dict(->'active'?:?$bool)""",
-                                     dict(bool=bool, dict=dict, str=str, uri=URIPredicate(), commit=CommitPredicate()))
-
-CONFIG_TITLE_SANITIZE = abdl.compile("""->title'title'?:?$str""", dict(str=str))
-CONFIG_BASE_URL_SANITIZE = abdl.compile("""->base_url'base_url'?:?$str:?$uri""", dict(str=str, uri=URIPredicate()))
-
-# modern matchers, raise ValidationError if the data doesn't exist.
-# they still skip "bad" entries, just like the old matchers.
-
-_MATCHER_REPOS = abdl.compile("""->'projects':$dict
-                                   ->commit[:?$str:?$commit]:?$dict
-                                     ->url[:?$str:?$uri]:?$dict
-                                       ->branch:?$dict
-                                         (->active'active'?:?$bool)
-                                         (->federate'federate'?:?$bool)?
-                                         (->pinned'pinned'?:?$bool)?""",
-                              dict(bool=bool, dict=dict, str=str, uri=URIPredicate(), commit=CommitPredicate()))
-_MATCHER_REPO_LIST_SRCS = abdl.compile("""->'repo_list_srcs':$dict
-                                            ->src[:?$str:?$uri]:?$dict
-                                              (->'active'?:?$bool)""",
-                                       dict(bool=bool, list=list, dict=dict, str=str, uri=URIPredicate(schemes=('https','file',))))
 # TODO
 #_MATCHER_ALIASES = abdl.compile("""->'project_settings':$dict
 #                                     ->commit/[0-9a-fA-F]{40}|[0-9a-fA-F]{64}/?:?$dict
@@ -107,10 +88,6 @@ _MATCHER_REPO_LIST_SRCS = abdl.compile("""->'repo_list_srcs':$dict
 #                                           (->'active'?:?$bool)""",
 #                                    dict(dict=dict, str=str, bool=bool))
 
-_MATCHER_TITLE = abdl.compile("""->title'title':$str""", dict(str=str))
-_MATCHER_BASE_URL = abdl.compile("""->base_url'base_url':$str:$uri""", dict(str=str, uri=URIPredicate()))
-_MATCHER_FEDITO = abdl.compile("""->fedito'fedi-to':$int""", dict(int=int))
-
 class OverridableProperty(abc.ABC):
     """An overridable property, with options.
 
@@ -277,13 +254,50 @@ class ObjectDataSource(DataSource):
     Updates to the backing object will be immediately reflected in this
     DataSource.
     """
+    # these must all be generators...
     _SUPPORTED_PROPERTIES = {
-                                DataProperty.INSTANCE_TITLE: lambda obj: (d['title'][1] for d in _MATCHER_TITLE.match(obj)),
-                                DataProperty.INSTANCE_BASE_URL: lambda obj: (d['base_url'][1] for d in _MATCHER_BASE_URL.match(obj)),
-                                DataProperty.INSTANCE_FEDITO: lambda obj: (d['fedito'][1] for d in _MATCHER_FEDITO.match(obj)),
-                                DataProperty.VCS_REPOS: lambda obj: (PCTP(r['commit'][0], r['url'][0], r['branch'][0], {k: v[1] for k, v in r.items() if k in {'active', 'federate', 'pinned'}}) for r in _MATCHER_REPOS.match(obj)),
-                                DataProperty.REPO_LIST_SOURCES: lambda obj: (RepoListSource(d['src'][0], d['src'][1]) for d in _MATCHER_REPO_LIST_SRCS.match(obj)),
-                            }
+        DataProperty.INSTANCE_TITLE:
+            lambda obj: (yield _check_type(obj.get('title'), str)),
+        DataProperty.INSTANCE_BASE_URL:
+            lambda obj: (yield _check_uri(obj.get('base_url'))),
+        DataProperty.INSTANCE_FEDITO:
+            lambda obj: (yield _check_type(obj.get('fedi-to'), int)),
+        DataProperty.VCS_REPOS:
+            lambda obj: (
+                PCTP(commit, uri, branch,
+                     {k: v
+                      for k, v in options.items()
+                      if (k in {'active', 'federate', 'pinned'}
+                          and isinstance(v, bool))
+                     })
+                for lazy_obj in (obj,)
+                for (commit, uris) in _check_type(lazy_obj.get('projects'),
+                                                  dict).items()
+                if isinstance(commit, str)
+                if _is_commit(commit)
+                if isinstance(uris, dict)
+                for (uri, branches) in uris.items()
+                if isinstance(uri, str)
+                if _is_uri(uri)
+                if isinstance(branches, dict)
+                for (branch, options) in branches.items()
+                if isinstance(options, dict)
+                if isinstance(options.get('active'), bool)
+            ),
+        DataProperty.REPO_LIST_SOURCES:
+            lambda obj: (
+                RepoListSource(src, options)
+                for lazy_obj in (obj,)
+                for (src, options) in _check_type(lazy_obj.get('repo_list_srcs'),
+                                                  dict).items()
+                if isinstance(src, str)
+                if _is_uri(src, schemes=('https','file'))
+                if isinstance(options, dict)
+                if isinstance(options.get('active'), bool)
+                # TODO it would probably make sense to add
+                # options.get('type', 'toml') somewhere...
+            ),
+    }
 
     def __init__(self, obj):
         self._obj = obj
@@ -297,13 +311,18 @@ class ObjectDataSource(DataSource):
     def get_property_values(self, prop):
         try:
             factory = self.get_supported_properties()[prop]
-        except KeyError as exc: raise PropertyError from exc
+        except KeyError as exc:
+            raise PropertyError from exc
         iterator = factory(self._obj)
         try:
             first = next(iterator)
-        except StopIteration: return (x for x in ())
-        except abdl.exceptions.ValidationError as exc: raise LookupError from exc
-        except LookupError as exc: raise RuntimeError from exc  # don't accidentally swallow bugs in the iterator
+        except StopIteration:
+            return (x for x in ())
+        except _ValidationError as exc:
+            raise LookupError from exc
+        except LookupError as exc:
+            # don't accidentally swallow bugs in the iterator
+            raise RuntimeError from exc
         return itertools.chain([first], iterator)
 
     @classmethod
@@ -322,10 +341,10 @@ class LocalDataSource(ObjectDataSource):
             updtime = self.last_updated
             self.last_updated = os.stat(self.filename).st_mtime
             if not self.file_exists or updtime != self.last_updated:
-                with open(self.filename, 'r', encoding='utf-8', newline='') as f:
-                    self._obj = qtoml.load(f)
+                with open(self.filename, 'rb') as f:
+                    self._obj = tomllib.load(f)
             self.file_exists = True
-        except (OSError, UnicodeDecodeError, qtoml.decoder.TOMLDecodeError) as e:
+        except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError) as e:
             self.file_exists = False
             self.last_updated = None
             self._obj = {}
@@ -348,6 +367,8 @@ class RemoteDataSource(ObjectDataSource):
         if self.next_update > time.time():
             return
         # I long for the day when toml has a registered media type
+        # FIXME this should be JSON
+        # (also doesn't it have one nowadays? -- nvm, not a registered one :/)
         response = requests.get(self.uri, headers={'user-agent': 'ganarchy/0.0.0', 'accept': '*/*'})
         self.remote_exists = response.status_code == 200
         seconds = 3600
@@ -364,8 +385,8 @@ class RemoteDataSource(ObjectDataSource):
         if self.remote_exists:
             response.encoding = 'utf-8'
             try:
-                self._obj = qtoml.loads(response.text)
-            except (UnicodeDecodeError, qtoml.decoder.TOMLDecodeError) as e:
+                self._obj = tomllib.loads(response.text)
+            except (UnicodeDecodeError, tomllib.TOMLDecodeError) as e:
                 self._obj = {}
                 return e
         else:
diff --git a/requirements.txt b/requirements.txt
index 3d7e7f6..e46aafc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,6 @@
 -e git+https://soniex2.autistic.space/git-repos/abdl.git@ff3628c36eab5ec19ab850e9126a951f5c203568#egg=gan0f74bd87a23b515b45da7e6f5d9cc82380443dab
 Click==7.0
 Jinja2==2.11.1
-qtoml==0.2.4
 requests==2.22.0
 
 certifi==2019.11.28