Compare commits
No commits in common. "ce79346f76c11d0473bcf7378d05870cef850b64" and "1bfab90d35a6e5739c824f8f0d830b2156e7eb7f" have entirely different histories.
ce79346f76
...
1bfab90d35
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -5,17 +5,6 @@ All notable changes to WuttJamaican will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## v0.20.0 (2025-01-11)
|
|
||||||
|
|
||||||
### Feat
|
|
||||||
|
|
||||||
- add basic support for "reports" feature
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- add `render_percent()` method for app handler
|
|
||||||
- set global default sender to root@localhost
|
|
||||||
|
|
||||||
## v0.19.3 (2025-01-09)
|
## v0.19.3 (2025-01-09)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
``wuttjamaican.reports``
|
|
||||||
========================
|
|
||||||
|
|
||||||
.. automodule:: wuttjamaican.reports
|
|
||||||
:members:
|
|
|
@ -247,33 +247,6 @@ Glossary
|
||||||
portion of the :term:`app`. Similar to a "plugin" concept; see
|
portion of the :term:`app`. Similar to a "plugin" concept; see
|
||||||
:doc:`narr/providers/index`.
|
:doc:`narr/providers/index`.
|
||||||
|
|
||||||
report
|
|
||||||
The concept of a report is intentionally vague, in the context of
|
|
||||||
WuttJamaican. Basically it is something which can be "ran"
|
|
||||||
(usually with :term:`report params`) to generate a data set. The
|
|
||||||
output can be viewed in the app UI, or it can be saved to file.
|
|
||||||
|
|
||||||
The base class is :class:`~wuttjamaican.reports.Report`. See
|
|
||||||
also :term:`report handler`.
|
|
||||||
|
|
||||||
report handler
|
|
||||||
The :term:`handler` responsible for running :term:`reports
|
|
||||||
<report>`, for display in app UI or saved to file etc.
|
|
||||||
|
|
||||||
Base class is :class:`~wuttjamaican.reports.ReportHandler`.
|
|
||||||
|
|
||||||
report key
|
|
||||||
Unique key which identifies a particular :term:`report`.
|
|
||||||
|
|
||||||
report module
|
|
||||||
This refers to a Python module which contains :term:`report`
|
|
||||||
definitions.
|
|
||||||
|
|
||||||
report params
|
|
||||||
This refers to the input parameters used when running a
|
|
||||||
:term:`report`. It is usually a simple mapping of key/value
|
|
||||||
pairs.
|
|
||||||
|
|
||||||
settings table
|
settings table
|
||||||
Table in the :term:`app database` which is used to store
|
Table in the :term:`app database` which is used to store
|
||||||
:term:`config settings<config setting>`. See also
|
:term:`config settings<config setting>`. See also
|
||||||
|
|
|
@ -87,6 +87,5 @@ Contents
|
||||||
api/wuttjamaican.install
|
api/wuttjamaican.install
|
||||||
api/wuttjamaican.people
|
api/wuttjamaican.people
|
||||||
api/wuttjamaican.progress
|
api/wuttjamaican.progress
|
||||||
api/wuttjamaican.reports
|
|
||||||
api/wuttjamaican.testing
|
api/wuttjamaican.testing
|
||||||
api/wuttjamaican.util
|
api/wuttjamaican.util
|
||||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttJamaican"
|
name = "WuttJamaican"
|
||||||
version = "0.20.0"
|
version = "0.19.3"
|
||||||
description = "Base package for Wutta Framework"
|
description = "Base package for Wutta Framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
||||||
|
|
|
@ -88,7 +88,6 @@ class AppHandler:
|
||||||
default_email_handler_spec = 'wuttjamaican.email:EmailHandler'
|
default_email_handler_spec = 'wuttjamaican.email:EmailHandler'
|
||||||
default_install_handler_spec = 'wuttjamaican.install:InstallHandler'
|
default_install_handler_spec = 'wuttjamaican.install:InstallHandler'
|
||||||
default_people_handler_spec = 'wuttjamaican.people:PeopleHandler'
|
default_people_handler_spec = 'wuttjamaican.people:PeopleHandler'
|
||||||
default_report_handler_spec = 'wuttjamaican.reports:ReportHandler'
|
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -772,22 +771,6 @@ class AppHandler:
|
||||||
"""
|
"""
|
||||||
return simple_error(error)
|
return simple_error(error)
|
||||||
|
|
||||||
def render_percent(self, value, decimals=2):
|
|
||||||
"""
|
|
||||||
Return a human-friendly display string for the given
|
|
||||||
percentage value, e.g. ``23.45139`` becomes ``"23.45 %"``.
|
|
||||||
|
|
||||||
:param value: The value to be rendered.
|
|
||||||
|
|
||||||
:returns: Display string for the percentage value.
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return ""
|
|
||||||
fmt = f'{{:0.{decimals}f}} %'
|
|
||||||
if value < 0:
|
|
||||||
return f'({fmt.format(-value)})'
|
|
||||||
return fmt.format(value)
|
|
||||||
|
|
||||||
def render_quantity(self, value, empty_zero=False):
|
def render_quantity(self, value, empty_zero=False):
|
||||||
"""
|
"""
|
||||||
Return a human-friendly display string for the given quantity
|
Return a human-friendly display string for the given quantity
|
||||||
|
@ -896,19 +879,6 @@ class AppHandler:
|
||||||
self.handlers['people'] = factory(self.config, **kwargs)
|
self.handlers['people'] = factory(self.config, **kwargs)
|
||||||
return self.handlers['people']
|
return self.handlers['people']
|
||||||
|
|
||||||
def get_report_handler(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Get the configured :term:`report handler`.
|
|
||||||
|
|
||||||
:rtype: :class:`~wuttjamaican.reports.ReportHandler`
|
|
||||||
"""
|
|
||||||
if 'reports' not in self.handlers:
|
|
||||||
spec = self.config.get(f'{self.appname}.reports.handler_spec',
|
|
||||||
default=self.default_report_handler_spec)
|
|
||||||
factory = self.load_object(spec)
|
|
||||||
self.handlers['reports'] = factory(self.config, **kwargs)
|
|
||||||
return self.handlers['reports']
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# convenience delegators
|
# convenience delegators
|
||||||
##############################
|
##############################
|
||||||
|
|
|
@ -1,228 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttJamaican -- Base package for Wutta Framework
|
|
||||||
# Copyright © 2023-2024 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of Wutta Framework.
|
|
||||||
#
|
|
||||||
# Wutta Framework is free software: you can redistribute it and/or modify it
|
|
||||||
# under the terms of the GNU General Public License as published by the Free
|
|
||||||
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
||||||
# later version.
|
|
||||||
#
|
|
||||||
# Wutta Framework 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 General Public License for
|
|
||||||
# more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Report Utilities
|
|
||||||
"""
|
|
||||||
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
from wuttjamaican.app import GenericHandler
|
|
||||||
|
|
||||||
|
|
||||||
class Report:
|
|
||||||
"""
|
|
||||||
Base class for all :term:`reports <report>`.
|
|
||||||
|
|
||||||
.. attribute:: report_key
|
|
||||||
|
|
||||||
Each report must define a unique key, to identify it.
|
|
||||||
|
|
||||||
.. attribute:: report_title
|
|
||||||
|
|
||||||
This is the common display title for the report.
|
|
||||||
"""
|
|
||||||
report_title = "Untitled Report"
|
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
self.config = config
|
|
||||||
self.app = config.get_app()
|
|
||||||
|
|
||||||
def add_params(self, schema):
|
|
||||||
"""
|
|
||||||
Add field nodes to the given schema, defining all
|
|
||||||
:term:`report params`.
|
|
||||||
|
|
||||||
:param schema: :class:`~colander:colander.Schema` instance.
|
|
||||||
|
|
||||||
The schema is from Colander so nodes must be compatible with
|
|
||||||
that; for instance::
|
|
||||||
|
|
||||||
import colander
|
|
||||||
|
|
||||||
def add_params(self, schema):
|
|
||||||
|
|
||||||
schema.add(colander.SchemaNode(
|
|
||||||
colander.Date(),
|
|
||||||
name='start_date'))
|
|
||||||
|
|
||||||
schema.add(colander.SchemaNode(
|
|
||||||
colander.Date(),
|
|
||||||
name='end_date'))
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_output_columns(self):
|
|
||||||
"""
|
|
||||||
This should return a list of column definitions to be used
|
|
||||||
when displaying or persisting the data output.
|
|
||||||
|
|
||||||
Each entry can be a simple column name, or else a dict with
|
|
||||||
other options, e.g.::
|
|
||||||
|
|
||||||
def get_output_columns(self):
|
|
||||||
return [
|
|
||||||
'foo',
|
|
||||||
{'name': 'bar',
|
|
||||||
'label': "BAR"},
|
|
||||||
{'name': 'sales',
|
|
||||||
'label': "Total Sales",
|
|
||||||
'numeric': True,
|
|
||||||
'formatter': self.app.render_currency},
|
|
||||||
]
|
|
||||||
|
|
||||||
:returns: List of column definitions as described above.
|
|
||||||
|
|
||||||
The last entry shown above has all options currently
|
|
||||||
supported; here we explain those:
|
|
||||||
|
|
||||||
* ``name`` - True name for the column.
|
|
||||||
|
|
||||||
* ``label`` - Display label for the column. If not specified,
|
|
||||||
one is derived from the ``name``.
|
|
||||||
|
|
||||||
* ``numeric`` - Boolean indicating the column data is numeric,
|
|
||||||
so should be right-aligned.
|
|
||||||
|
|
||||||
* ``formatter`` - Custom formatter / value rendering callable
|
|
||||||
for the column. If set, this will be called with just one
|
|
||||||
arg (the value) for each data row.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def make_data(self, params, progress=None):
|
|
||||||
"""
|
|
||||||
This must "run" the report and return the final data.
|
|
||||||
|
|
||||||
Note that this should *not* (usually) write the data to file,
|
|
||||||
its purpose is just to obtain the data.
|
|
||||||
|
|
||||||
The return value should usually be a dict, with no particular
|
|
||||||
structure required beyond that. However it also can be a list
|
|
||||||
of data rows.
|
|
||||||
|
|
||||||
There is no default logic here; subclass must define.
|
|
||||||
|
|
||||||
:param params: Dict of :term:`report params`.
|
|
||||||
|
|
||||||
:param progress: Optional progress indicator factory.
|
|
||||||
|
|
||||||
:returns: Data dict, or list of rows.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class ReportHandler(GenericHandler):
|
|
||||||
"""
|
|
||||||
Base class and default implementation for the :term:`report
|
|
||||||
handler`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_report_modules(self):
|
|
||||||
"""
|
|
||||||
Returns a list of all known :term:`report modules <report
|
|
||||||
module>`.
|
|
||||||
|
|
||||||
This will discover all report modules exposed by the
|
|
||||||
:term:`app`, and/or its :term:`providers <provider>`.
|
|
||||||
"""
|
|
||||||
if not hasattr(self, '_report_modules'):
|
|
||||||
self._report_modules = []
|
|
||||||
for provider in self.app.providers.values():
|
|
||||||
if hasattr(provider, 'report_modules'):
|
|
||||||
modules = provider.report_modules
|
|
||||||
if modules:
|
|
||||||
if isinstance(modules, str):
|
|
||||||
modules = [modules]
|
|
||||||
for module in modules:
|
|
||||||
module = importlib.import_module(module)
|
|
||||||
self._report_modules.append(module)
|
|
||||||
|
|
||||||
return self._report_modules
|
|
||||||
|
|
||||||
def get_reports(self):
|
|
||||||
"""
|
|
||||||
Returns a dict of all known :term:`reports <report>`, keyed by
|
|
||||||
:term:`report key`.
|
|
||||||
|
|
||||||
This calls :meth:`get_report_modules()` and for each module,
|
|
||||||
it discovers all the reports it contains.
|
|
||||||
"""
|
|
||||||
if not hasattr(self, '_reports'):
|
|
||||||
self._reports = {}
|
|
||||||
for module in self.get_report_modules():
|
|
||||||
for name in dir(module):
|
|
||||||
obj = getattr(module, name)
|
|
||||||
if (isinstance(obj, type)
|
|
||||||
and obj is not Report
|
|
||||||
and issubclass(obj, Report)):
|
|
||||||
self._reports[obj.report_key] = obj
|
|
||||||
|
|
||||||
return self._reports
|
|
||||||
|
|
||||||
def get_report(self, key, instance=True):
|
|
||||||
"""
|
|
||||||
Fetch the :term:`report` class or instance for given key.
|
|
||||||
|
|
||||||
:param key: Identifying :term:`report key`.
|
|
||||||
|
|
||||||
:param instance: Whether to return the class, or an instance.
|
|
||||||
Default is ``True`` which means return the instance.
|
|
||||||
|
|
||||||
:returns: :class:`Report` class or instance, or ``None`` if
|
|
||||||
the report could not be found.
|
|
||||||
"""
|
|
||||||
reports = self.get_reports()
|
|
||||||
if key in reports:
|
|
||||||
report = reports[key]
|
|
||||||
if instance:
|
|
||||||
report = report(self.config)
|
|
||||||
return report
|
|
||||||
|
|
||||||
def make_report_data(self, report, params=None, progress=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Run the given report and return the output data.
|
|
||||||
|
|
||||||
This calls :meth:`Report.make_data()` on the report, and
|
|
||||||
tweaks the output as needed for consistency. The return value
|
|
||||||
should resemble this structure::
|
|
||||||
|
|
||||||
{
|
|
||||||
'output_title': "My Report",
|
|
||||||
'data': ...,
|
|
||||||
}
|
|
||||||
|
|
||||||
However that is the *minimum*; the dict may have other keys as
|
|
||||||
well.
|
|
||||||
|
|
||||||
:param report: :class:`Report` instance to run.
|
|
||||||
|
|
||||||
:param params: Dict of :term:`report params`.
|
|
||||||
|
|
||||||
:param progress: Optional progress indicator factory.
|
|
||||||
|
|
||||||
:returns: Data dict with structure shown above.
|
|
||||||
"""
|
|
||||||
data = report.make_data(params or {}, progress=progress, **kwargs)
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
data = {'data': data}
|
|
||||||
data.setdefault('output_title', report.report_title)
|
|
||||||
return data
|
|
|
@ -1 +0,0 @@
|
||||||
# nb. some tests require this to be a true package
|
|
|
@ -491,22 +491,6 @@ app_title = WuttaTest
|
||||||
result = self.app.render_error(error)
|
result = self.app.render_error(error)
|
||||||
self.assertEqual(result, "RuntimeError")
|
self.assertEqual(result, "RuntimeError")
|
||||||
|
|
||||||
def test_render_percent(self):
|
|
||||||
|
|
||||||
# null
|
|
||||||
self.assertEqual(self.app.render_percent(None), "")
|
|
||||||
|
|
||||||
# typical
|
|
||||||
self.assertEqual(self.app.render_percent(12.3419), '12.34 %')
|
|
||||||
|
|
||||||
# more decimal places
|
|
||||||
self.assertEqual(self.app.render_percent(12.3419, decimals=3), '12.342 %')
|
|
||||||
self.assertEqual(self.app.render_percent(12.3419, decimals=4), '12.3419 %')
|
|
||||||
|
|
||||||
# negative
|
|
||||||
self.assertEqual(self.app.render_percent(-12.3419), '(12.34 %)')
|
|
||||||
self.assertEqual(self.app.render_percent(-12.3419, decimals=3), '(12.342 %)')
|
|
||||||
|
|
||||||
def test_render_quantity(self):
|
def test_render_quantity(self):
|
||||||
|
|
||||||
# null
|
# null
|
||||||
|
@ -573,12 +557,6 @@ app_title = WuttaTest
|
||||||
people = self.app.get_people_handler()
|
people = self.app.get_people_handler()
|
||||||
self.assertIsInstance(people, PeopleHandler)
|
self.assertIsInstance(people, PeopleHandler)
|
||||||
|
|
||||||
def test_get_report_handler(self):
|
|
||||||
from wuttjamaican.reports import ReportHandler
|
|
||||||
|
|
||||||
handler = self.app.get_report_handler()
|
|
||||||
self.assertIsInstance(handler, ReportHandler)
|
|
||||||
|
|
||||||
def test_send_email(self):
|
def test_send_email(self):
|
||||||
from wuttjamaican.email import EmailHandler
|
from wuttjamaican.email import EmailHandler
|
||||||
|
|
||||||
|
|
|
@ -151,7 +151,7 @@ class TestEmailHandler(ConfigTestCase):
|
||||||
self.assertEqual(len(modules), 1)
|
self.assertEqual(len(modules), 1)
|
||||||
self.assertIs(modules[0], mod)
|
self.assertIs(modules[0], mod)
|
||||||
|
|
||||||
# provider may specify modules as string
|
# provider may specify modules as list
|
||||||
providers = {
|
providers = {
|
||||||
'wuttatest': MagicMock(email_modules='wuttjamaican.email'),
|
'wuttatest': MagicMock(email_modules='wuttjamaican.email'),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
from wuttjamaican import reports as mod
|
|
||||||
from wuttjamaican.testing import ConfigTestCase
|
|
||||||
|
|
||||||
|
|
||||||
class MockFooReport(mod.Report):
|
|
||||||
report_key = 'mock_foo'
|
|
||||||
report_title = "MOCK Report"
|
|
||||||
|
|
||||||
def make_data(self, params, **kwargs):
|
|
||||||
return [
|
|
||||||
{'foo': 'bar'},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class TestReport(ConfigTestCase):
|
|
||||||
|
|
||||||
def test_get_output_columns(self):
|
|
||||||
report = mod.Report(self.config)
|
|
||||||
self.assertRaises(NotImplementedError, report.get_output_columns)
|
|
||||||
|
|
||||||
def test_make_data(self):
|
|
||||||
report = mod.Report(self.config)
|
|
||||||
self.assertRaises(NotImplementedError, report.make_data, {})
|
|
||||||
|
|
||||||
|
|
||||||
class TestReportHandler(ConfigTestCase):
|
|
||||||
|
|
||||||
def make_handler(self):
|
|
||||||
return mod.ReportHandler(self.config)
|
|
||||||
|
|
||||||
def test_get_report_modules(self):
|
|
||||||
|
|
||||||
# no providers, no report modules
|
|
||||||
with patch.object(self.app, 'providers', new={}):
|
|
||||||
handler = self.make_handler()
|
|
||||||
self.assertEqual(handler.get_report_modules(), [])
|
|
||||||
|
|
||||||
# provider may specify modules as list
|
|
||||||
providers = {
|
|
||||||
'wuttatest': MagicMock(report_modules=['wuttjamaican.reports']),
|
|
||||||
}
|
|
||||||
with patch.object(self.app, 'providers', new=providers):
|
|
||||||
handler = self.make_handler()
|
|
||||||
modules = handler.get_report_modules()
|
|
||||||
self.assertEqual(len(modules), 1)
|
|
||||||
self.assertIs(modules[0], mod)
|
|
||||||
|
|
||||||
# provider may specify modules as string
|
|
||||||
providers = {
|
|
||||||
'wuttatest': MagicMock(report_modules='wuttjamaican.reports'),
|
|
||||||
}
|
|
||||||
with patch.object(self.app, 'providers', new=providers):
|
|
||||||
handler = self.make_handler()
|
|
||||||
modules = handler.get_report_modules()
|
|
||||||
self.assertEqual(len(modules), 1)
|
|
||||||
self.assertIs(modules[0], mod)
|
|
||||||
|
|
||||||
def test_get_reports(self):
|
|
||||||
|
|
||||||
# no providers, no reports
|
|
||||||
with patch.object(self.app, 'providers', new={}):
|
|
||||||
handler = self.make_handler()
|
|
||||||
self.assertEqual(handler.get_reports(), {})
|
|
||||||
|
|
||||||
# provider may define reports (via modules)
|
|
||||||
providers = {
|
|
||||||
'wuttatest': MagicMock(report_modules=['tests.test_reports']),
|
|
||||||
}
|
|
||||||
with patch.object(self.app, 'providers', new=providers):
|
|
||||||
handler = self.make_handler()
|
|
||||||
reports = handler.get_reports()
|
|
||||||
self.assertEqual(len(reports), 1)
|
|
||||||
self.assertIn('mock_foo', reports)
|
|
||||||
|
|
||||||
def test_get_report(self):
|
|
||||||
providers = {
|
|
||||||
'wuttatest': MagicMock(report_modules=['tests.test_reports']),
|
|
||||||
}
|
|
||||||
|
|
||||||
with patch.object(self.app, 'providers', new=providers):
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
# as instance
|
|
||||||
report = handler.get_report('mock_foo')
|
|
||||||
self.assertIsInstance(report, mod.Report)
|
|
||||||
self.assertIsInstance(report, MockFooReport)
|
|
||||||
|
|
||||||
# as class
|
|
||||||
report = handler.get_report('mock_foo', instance=False)
|
|
||||||
self.assertTrue(issubclass(report, mod.Report))
|
|
||||||
self.assertIs(report, MockFooReport)
|
|
||||||
|
|
||||||
def test_make_report_data(self):
|
|
||||||
providers = {
|
|
||||||
'wuttatest': MagicMock(report_modules=['tests.test_reports']),
|
|
||||||
}
|
|
||||||
|
|
||||||
with patch.object(self.app, 'providers', new=providers):
|
|
||||||
handler = self.make_handler()
|
|
||||||
report = handler.get_report('mock_foo')
|
|
||||||
|
|
||||||
data = handler.make_report_data(report)
|
|
||||||
self.assertEqual(len(data), 2)
|
|
||||||
self.assertIn('output_title', data)
|
|
||||||
self.assertEqual(data['output_title'], "MOCK Report")
|
|
||||||
self.assertIn('data', data)
|
|
||||||
self.assertEqual(data['data'], [{'foo': 'bar'}])
|
|
Loading…
Reference in a new issue