Changeset - ca94ecafb02d
[Not reviewed]
0 2 0
Brett Smith - 3 years ago 2021-03-18 20:22:30
brettcsmith@brettcsmith.org
query: Add db_name column.
2 files changed with 9 insertions and 1 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/query.py
Show inline comments
...
 
@@ -179,96 +179,102 @@ class DBColumn(bc_query_compile.EvalColumn):
 
    def __init_subclass__(cls) -> None:
 
        if issubclass(cls._dtype, set):
 
            cls._return = cls._return_set
 
        else:
 
            cls._return = cls._return_scalar
 

	
 
    def __init__(self, colname: Optional[str]=None) -> None:
 
        if not hasattr(self, '_db_cursor'):
 
            if colname is None:
 
                colname = type(self).__name__.lower().replace('db', 'db_', 1)
 
            raise RuntimeError(f"no entity database loaded - {colname} not available")
 
        super().__init__(self._dtype)
 

	
 
    def _entity(self, meta: data.PostingMeta) -> str:
 
        entity = meta.get('entity')
 
        return entity if isinstance(entity, str) else '\0'
 

	
 
    def _return_scalar(self) -> object:
 
        row = self._db_cursor.fetchone()
 
        return self._dtype() if row is None else self._dtype(row[0])
 

	
 
    def _return_set(self) -> object:
 
        return self._dtype(value for value, in self._db_cursor)
 

	
 
    def __call__(self, context: PostingContext) -> object:
 
        entity = self._entity(ContextMeta(context))
 
        self._db_cursor.execute(self._db_query, (entity,))
 
        return self._return()
 

	
 

	
 
class DBEmail(DBColumn):
 
    """Look up an entity's email addresses from the database"""
 
    _db_query = """
 
SELECT email.email_address
 
FROM donor
 
JOIN donor_email_address_mapping map ON donor.id = map.donor_id
 
JOIN email_address email ON map.email_address_id = email.id
 
WHERE donor.ledger_entity_id = ?
 
ORDER BY email.date_encountered DESC
 
"""
 

	
 

	
 
class DBId(DBColumn):
 
    """Look up an entity's numeric id from the database"""
 
    _db_query = "SELECT id FROM donor WHERE ledger_entity_id = ?"
 
    _dtype = int
 

	
 

	
 
class DBName(DBColumn):
 
    """Look up an entity's display name from the database"""
 
    _db_query = "SELECT display_name FROM donor WHERE ledger_entity_id = ?"
 
    _dtype = str
 

	
 

	
 
class DBPostal(DBColumn):
 
    """Look up an entity's postal addresses from the database"""
 
    _db_query = """
 
SELECT postal.formatted_address
 
FROM donor
 
JOIN donor_postal_address_mapping map ON donor.id = map.donor_id
 
JOIN postal_address postal ON map.postal_address_id = postal.id
 
WHERE donor.ledger_entity_id = ?
 
ORDER BY postal.date_encountered DESC
 
"""
 

	
 

	
 
class MetaDocs(bc_query_env.AnyMeta):
 
    """Return a list of document links from metadata."""
 
    def __init__(self, operands: List[bc_query_compile.EvalNode]) -> None:
 
        super(bc_query_env.AnyMeta, self).__init__(operands, set)
 
        # The second argument is our return type.
 
        # It should match the annotated return type of __call__.
 

	
 
    def __call__(self, context: PostingContext) -> Set[str]:
 
        raw_value = super().__call__(context)
 
        seq = raw_value.split() if isinstance(raw_value, str) else ''
 
        return set(seq)
 

	
 

	
 
class RTField(NamedTuple):
 
    key: str
 
    parse: Optional[Callable[[str], object]]
 
    unset_value: Optional[str] = None
 

	
 
    def load(self, rt_ticket: RTResult) -> object:
 
        value = rt_ticket.get(self.key) if rt_ticket else None
 
        if not value or value == self.unset_value:
 
            return None
 
        elif self.parse is None:
 
            return value
 
        else:
 
            return self.parse(value)
 

	
 

	
 
class RTTicket(bc_query_compile.EvalFunction):
 
    """Look up a field from RT ticket(s) mentioned in metadata documentation"""
 
    __intypes__ = [str, str, int]
 
    _CF_REGEXPS = [
 
        re.compile(r'^CF_([-\w]+)$', re.IGNORECASE),
 
        re.compile(r'^CF\.\{([-\w]+)\}$', re.IGNORECASE),
 
    ]
 
    FIELDS = {key: RTField(key, None) for key in [
...
 
@@ -399,100 +405,102 @@ class StrMeta(bc_query_env.AnyMeta):
 

	
 

	
 
class AggregateSet(bc_query_compile.EvalAggregator):
 
    """Filter argument values that aren't unique."""
 
    __intypes__ = [object]
 

	
 
    def __init__(self, operands: List[bc_query_compile.EvalNode]) -> None:
 
       super().__init__(operands, set)
 

	
 
    def allocate(self, allocator: bc_query_execute.Allocator) -> None:
 
        """Allocate and save an index handle into result storage."""
 
        self.handle = allocator.allocate()
 

	
 
    def initialize(self, store: Store) -> None:
 
        """Prepare result storage for a new aggregation."""
 
        store[self.handle] = self.dtype()
 
        # self.dtype() is our return type, aka the second argument to __init__
 
        # above, aka the annotated return type of __call__.
 

	
 
    def update(self, store: Store, context: PostingContext) -> None:
 
        """Update existing storage with new result data."""
 
        value, = self.eval_args(context)
 
        if isinstance(value, Iterable) and not isinstance(value, (str, tuple)):
 
            store[self.handle].update(value)
 
        else:
 
            store[self.handle].add(value)
 

	
 
    def __call__(self, context: PostingContext) -> set:
 
        """Return the result for an aggregation."""
 
        return context.store[self.handle]  # type:ignore[no-any-return]
 

	
 

	
 
class _EnvironmentMixin:
 
    db_path = Path('Financial', 'Ledger', 'supporters.db')
 
    columns: EnvironmentColumns
 
    functions: EnvironmentFunctions
 

	
 
    @classmethod
 
    def with_config(cls, config: configmod.Config) -> Type['_EnvironmentMixin']:
 
        columns = cls.columns.copy()
 
        repo_path = config.repository_path()
 
        try:
 
            if repo_path is None:
 
                raise sqlite3.Error("no repository configured to host database")
 
            db_conn = sqlite3.connect(os.fspath(repo_path / cls.db_path))
 
        except (OSError, sqlite3.Error):
 
            columns['db_email'] = DBEmail
 
            columns['db_id'] = DBId
 
            columns['db_name'] = DBName
 
            columns['db_postal'] = DBPostal
 
        else:
 
            columns['db_email'] = DBEmail.with_db(db_conn)
 
            columns['db_id'] = DBId.with_db(db_conn)
 
            columns['db_name'] = DBName.with_db(db_conn)
 
            columns['db_postal'] = DBPostal.with_db(db_conn)
 

	
 
        rt_credentials = config.rt_credentials()
 
        rt_client = config.rt_client(rt_credentials)
 
        if rt_client is None:
 
            rt_ticket = RTTicket
 
        else:
 
            rt_ticket = RTTicket.with_client(rt_client, rt_credentials.idstr())
 
        functions = cls.functions.copy()
 
        functions[('rt_ticket', str, str)] = rt_ticket
 
        functions[('rt_ticket', str, str, int)] = rt_ticket
 
        return type(cls.__name__, (cls,), {
 
            'columns': columns,
 
            'functions': functions,
 
        })
 

	
 

	
 
class FilterPostingsEnvironment(bc_query_env.FilterPostingsEnvironment, _EnvironmentMixin):
 
    columns: EnvironmentColumns  # type:ignore[assignment]
 
    functions: EnvironmentFunctions = bc_query_env.FilterPostingsEnvironment.functions.copy()  # type:ignore[assignment]
 
    functions['meta_docs'] = MetaDocs
 
    functions['str_meta'] = StrMeta
 

	
 

	
 
class TargetsEnvironment(bc_query_env.TargetsEnvironment, _EnvironmentMixin):
 
    columns: EnvironmentColumns  # type:ignore[assignment]
 
    functions: EnvironmentFunctions = FilterPostingsEnvironment.functions.copy()  # type:ignore[assignment]
 
    functions.update(bc_query_env.AGGREGATOR_FUNCTIONS)
 
    functions['set'] = AggregateSet
 

	
 

	
 
class BooksLoader:
 
    """Closure to load books with a zero-argument callable
 

	
 
    This matches the load interface that BQLShell expects.
 
    """
 
    def __init__(
 
            self,
 
            books_loader: Optional[books.Loader],
 
            start_date: Optional[datetime.date]=None,
 
            stop_date: Optional[datetime.date]=None,
 
            rewrite_rules: Sequence[rewrite.RewriteRuleset]=(),
 
    ) -> None:
 
        self.books_loader = books_loader
 
        self.start_date = start_date
 
        self.stop_date = stop_date
 
        self.rewrite_rules = rewrite_rules
 

	
setup.py
Show inline comments
 
#!/usr/bin/env python3
 

	
 
from setuptools import setup
 

	
 
setup(
 
    name='conservancy_beancount',
 
    description="Plugin, library, and reports for reading Conservancy's books",
 
    version='1.19.5',
 
    version='1.19.6',
 
    author='Software Freedom Conservancy',
 
    author_email='info@sfconservancy.org',
 
    license='GNU AGPLv3+',
 

	
 
    install_requires=[
 
        'babel>=2.6',  # Debian:python3-babel
 
        'beancount>=2.2',  # Debian:beancount
 
        'GitPython>=2.0',  # Debian:python3-git
 
        # 1.4.1 crashes when trying to save some documents.
 
        'odfpy>=1.4.0,!=1.4.1',  # Debian:python3-odf
 
        'pdfminer.six>=20200101',
 
        'python-dateutil>=2.7',  # Debian:python3-dateutil
 
        'PyYAML>=3.0',  # Debian:python3-yaml
 
        'regex',  # Debian:python3-regex
 
        'rt>=2.0',
 
    ],
 
    setup_requires=[
 
        'pytest-mypy',
 
        'pytest-runner',  # Debian:python3-pytest-runner
 
    ],
 
    tests_require=[
 
        'mypy>=0.770',  # Debian:python3-mypy
 
        'pytest',  # Debian:python3-pytest
 
    ],
 

	
 
    packages=[
 
        'conservancy_beancount',
 
        'conservancy_beancount.pdfforms',
 
        'conservancy_beancount.pdfforms.extract',
 
        'conservancy_beancount.plugin',
 
        'conservancy_beancount.reconcile',
 
        'conservancy_beancount.reports',
 
        'conservancy_beancount.tools',
 
    ],
 
    entry_points={
 
        'console_scripts': [
 
            'accrual-report = conservancy_beancount.reports.accrual:entry_point',
 
            'assemble-audit-reports = conservancy_beancount.tools.audit_report:entry_point',
 
            'balance-sheet-report = conservancy_beancount.reports.balance_sheet:entry_point',
 
            'budget-report = conservancy_beancount.reports.budget:entry_point',
 
            'bean-sort = conservancy_beancount.tools.sort_entries:entry_point',
 
            'extract-odf-links = conservancy_beancount.tools.extract_odf_links:entry_point',
 
            'fund-report = conservancy_beancount.reports.fund:entry_point',
 
            'ledger-report = conservancy_beancount.reports.ledger:entry_point',
 
            'opening-balances = conservancy_beancount.tools.opening_balances:entry_point',
 
            'pdfform-extract = conservancy_beancount.pdfforms.extract:entry_point',
 
            'pdfform-extract-irs990scheduleA = conservancy_beancount.pdfforms.extract.irs990scheduleA:entry_point',
 
            'pdfform-fill = conservancy_beancount.pdfforms.fill:entry_point',
0 comments (0 inline, 0 general)