Skip to content

Commit a5cea5f

Browse files
committed
Changes needed to support pytest + refactor to make installable with pip
1 parent dd0ca14 commit a5cea5f

17 files changed

+187
-80
lines changed

README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ You only need the database connector(s) for the database system type(s) that you
2424

2525
## Configuration
2626

27-
Copy the files `*.example.cfg` files to just `*.cfg`:
27+
Copy the files `conf/*.example.cfg` files to just `conf/*.cfg`:
2828

2929
* `config.example.cfg` to `config.cfg`
3030
* `jobs.example.cfg` to `jobs.cfg`
@@ -52,3 +52,9 @@ You also need to add database credentials to the `datasources.cfg` file and emai
5252
If your database system is not yet supported, then you need to amend the `create_report` in the `DBReport` class.
5353

5454
See also the *.example.cfg files for examples.
55+
56+
To install the source code in editable mode for development:
57+
```bash
58+
git clone [email protected]:DiamondLightSource/db-jobs.git
59+
pip install --user -e db-jobs
60+
```

bin/runjob.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#
2+
# Copyright 2022 Karl Levik
3+
#
4+
5+
import argparse
6+
import logging
7+
import dbjobs
8+
9+
parser = argparse.ArgumentParser()
10+
parser.add_argument("-j", "--job", help = "Specify job")
11+
parser.add_argument("-d", "--dir", help = "Specify config dir")
12+
13+
args = parser.parse_args()
14+
15+
j = dbjobs.create("job", job_section=args.job, conf_dir=args.dir, log_level=logging.DEBUG)
16+
j.run_job()

bin/runreport.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#
2+
# Copyright 2021 Karl Levik
3+
#
4+
import argparse
5+
import logging
6+
import dbjobs
7+
8+
parser = argparse.ArgumentParser()
9+
parser.add_argument("-j", "--job", help = "Specify job")
10+
parser.add_argument("-d", "--dir", help = "Specify config dir")
11+
parser.add_argument("-i", "--interval", help = "Specify interval")
12+
parser.add_argument("-m", "--start_month", help = "Specify start month")
13+
parser.add_argument("-y", "--start_year", help = "Specify start year")
14+
15+
args = parser.parse_args()
16+
#r = DBReport(job_section=args.job, interval=args.interval, start_year=args.start_year, start_month=self.start_month, log_level=logging.DEBUG)
17+
r = dbjobs.create("report", job_section=args.job, conf_dir=args.dir, log_level=logging.DEBUG)
18+
r.make_sql(args.interval, args.start_year, args.start_month)
19+
r.run_job()
20+
r.send_email(f'{args.job} for {args.interval} starting {r.start_date}')
File renamed without changes.
File renamed without changes.
File renamed without changes.

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["setuptools>=42", "wheel"]
3+
build-backend = "setuptools.build_meta"

runjob.py

-9
This file was deleted.

runreport.py

-11
This file was deleted.

setup.cfg

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[metadata]
2+
name = dbjobs
3+
version = 0.5.0
4+
description = Python package to execute SQL statements and optionally generate reports from the result sets.
5+
description-file = README.md
6+
long_description = This package allows the execution of SQL statements. Several different database systems are supported. Optionally, the result sets (if any) can be used to create reports in either xlsx or csv format, and the reports can be emailed as attachments. SQL statements, database connecttion details and email preferences are stored in configuration files.
7+
author = Diamond Light Source
8+
license = Apache License, Version 2.0
9+
10+
[options]
11+
packages = find:
12+
package_dir =
13+
=src
14+
python_requires = >=3.7
15+
scripts =
16+
bin/runjob.py
17+
bin/runreport.py
18+
19+
[options.packages.find]
20+
where = src

setup.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import setuptools
2+
3+
if __name__ == "__main__":
4+
setuptools.setup()

src/dbjobs/__init__.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
__version__ = "0.5.0"
2+
3+
def create(type, job_section, conf_dir, log_level):
4+
if type == "job":
5+
from dbjobs.dbjob import DBJob
6+
return DBJob(job_section=job_section, conf_dir=conf_dir, log_level=log_level)
7+
elif type == "report":
8+
from dbjobs.dbreport import DBReport
9+
return DBReport(job_section=job_section, conf_dir=conf_dir, log_level=log_level)
10+
else:
11+
raise AttributeError(
12+
f"{type} is not a supported dbjob type. Supported types are 'job' and 'report'"
13+
)

src/dbjobs/base.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import sys
2+
3+
class Base(object):
4+
"""Base class for project"""
5+
6+
def error(self, msg):
7+
sys.exit(f"ERROR: {msg}")
8+
9+
def warning(self, msg):
10+
print(f"WARNING: {msg}")
11+
12+
def debug(self, msg):
13+
if self.debug_opt:
14+
print(f"DEBUG: {msg}")

dbjob.py src/dbjobs/dbjob.py

+22-19
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import atexit
66
from logging.handlers import RotatingFileHandler
77
from datetime import datetime, timedelta, date
8-
import sys, os, copy
8+
import sys, os, copy, os.path
9+
from dbjobs.base import Base
910
try:
1011
import pytds
1112
except ImportError:
@@ -19,27 +20,20 @@
1920
except ImportError:
2021
psycopg2 = None
2122

23+
import configparser
2224

23-
# Trick to make it work with both Python 2 and 3:
24-
try:
25-
import configparser
26-
except ImportError:
27-
import ConfigParser as configparser
28-
29-
class DBJob():
25+
class DBJob(Base):
3026
"""Utility methods to execute a database query with logging based on
3127
configuration and command-line parameters"""
3228

33-
def __init__(self, log_level=logging.DEBUG):
34-
if len(sys.argv) <= 1:
35-
msg = "No parameters"
36-
logging.getLogger().error(msg)
37-
raise AttributeError(msg)
29+
def __init__(self, job_section=None, conf_dir=None, log_level=logging.DEBUG):
30+
if job_section is None:
31+
self.error("Job section is required.")
3832

39-
self.read_config(sys.argv[1])
33+
self.read_config(job_section, conf_dir)
4034
self.working_dir = self.config['directory']
4135
self.fileprefix = self.job['file_prefix']
42-
self.set_logging(level = log_level, filepath = os.path.join(self.working_dir, '%s.log' % self.fileprefix))
36+
self.set_logging(level = log_level, filepath = os.path.join(self.working_dir, f"{self.fileprefix}.log"))
4337
logging.getLogger().info("DBJob %s started" % self.job['fullname'])
4438
atexit.register(self.clean_up)
4539
self.sql = self.job['sql']
@@ -157,13 +151,22 @@ def get_section_items(self, conf_file, conf_section):
157151

158152
return dict(config.items(conf_section))
159153

160-
def read_config(self, job_section):
154+
def read_config(self, job_section, conf_dir=None):
161155
"""Read the email settings, job configuration and DB credentials from
162156
the config.cfg, reports.cfg and datasources.cfg config files"""
163157

164-
self.config = self.get_section_items("config.cfg", job_section)
165-
self.job = self.get_section_items("jobs.cfg", job_section)
158+
conf_file = "config.cfg"
159+
jobs_file = "jobs.cfg"
160+
ds_file = "datasources.cfg"
161+
162+
if conf_dir:
163+
conf_file = os.path.join(conf_dir, conf_file)
164+
jobs_file = os.path.join(conf_dir, jobs_file)
165+
ds_file = os.path.join(conf_dir, ds_file)
166+
167+
self.config = self.get_section_items(conf_file, job_section)
168+
self.job = self.get_section_items(jobs_file, job_section)
166169
ds_section = self.job['datasource']
167-
self.datasource = self.get_section_items("datasources.cfg", ds_section)
170+
self.datasource = self.get_section_items(ds_file, ds_section)
168171

169172
return True

dbreport.py src/dbjobs/dbreport.py

+34-40
Original file line numberDiff line numberDiff line change
@@ -13,67 +13,61 @@
1313
import pytds
1414
import mysql.connector
1515
import psycopg2
16-
from dbjob import DBJob
16+
from dbjobs.dbjob import DBJob
1717

18-
# Trick to make it work with both Python 2 and 3:
19-
try:
20-
import configparser
21-
except ImportError:
22-
import ConfigParser as configparser
18+
import configparser
2319

2420
class DBReport(DBJob):
2521
"""Utility methods to create a report and send it as en email attachment"""
2622

27-
def __init__(self, log_level=logging.DEBUG):
28-
super().__init__(log_level=log_level)
29-
self.get_parameters()
30-
31-
nowstr = str(datetime.now().strftime('%Y%m%d-%H%M%S'))
23+
def __init__(self, job_section=None, conf_dir=None, log_level=logging.DEBUG):
24+
super().__init__(job_section=job_section, conf_dir=conf_dir, log_level=log_level)
3225
self.filesuffix = self.job['file_suffix']
33-
self.filename = '%s%s_%s-%s_%s%s' % (self.fileprefix, self.interval, self.start_year, self.start_month, nowstr, self.filesuffix)
34-
self.set_logging(level = log_level, filepath = os.path.join(self.working_dir, '%s%s_%s-%s.log' % (self.fileprefix, self.interval, self.start_year, self.start_month)))
3526

36-
def get_parameters(self):
27+
def get_start_date(self, interval=None, start_year=None, start_month=None):
3728
# Get input parameters, otherwise use default values
38-
self.interval = 'month'
39-
4029
today = date.today()
4130
first = today.replace(day=1)
4231
prev_date = first - timedelta(days=1)
4332

44-
if len(sys.argv) > 2:
45-
self.interval = sys.argv[2]
33+
if interval:
34+
self.interval = interval
35+
else:
36+
self.interval = "month"
4637

47-
if len(sys.argv) >= 2:
48-
if self.interval == 'month':
49-
self.start_year = prev_date.year
50-
self.start_month = prev_date.month
51-
elif self.interval == 'year':
52-
self.start_year = prev_date.year - 1
53-
self.start_month = prev_date.month
54-
else:
55-
err_msg = 'interval must be "month" or "year"'
56-
logging.getLogger().error(err_msg)
57-
raise AttributeError(err_msg)
38+
if self.interval == "month":
39+
self.start_year = prev_date.year
40+
self.start_month = prev_date.month
5841

59-
if len(sys.argv) > 3:
60-
self.start_year = sys.argv[3] # e.g. 2018
61-
if len(sys.argv) > 4:
62-
self.start_month = sys.argv[4] # e.g. 02
42+
elif self.interval == "year":
43+
self.start_year = prev_date.year - 1
44+
self.start_month = prev_date.month
6345

64-
self.start_date = '%s/%s/01' % (self.start_year, self.start_month)
46+
else:
47+
self.error('interval must be "month" or "year"')
48+
49+
if start_year:
50+
self.start_year = start_year
51+
if start_month:
52+
self.start_month = start_month
53+
54+
return f'{self.start_year}/{self.start_month}/01'
6555

66-
def read_config(self, job_section):
67-
super().read_config(job_section)
56+
def read_config(self, job_section, conf_dir):
57+
super().read_config(job_section, conf_dir=conf_dir)
6858
self.sender = self.config['sender']
6959
self.recipients = self.config['recipients']
7060

71-
def make_sql(self):
61+
def make_sql(self, interval, start_year, start_month):
7262
"""Create proper SQL from the template - merge the headers in as aliases"""
7363
self.headers = self.job['sql_headers'].split(',')
7464
fmt = copy.deepcopy(self.headers)
65+
66+
self.start_date = self.get_start_date(interval, start_year, start_month)
7567
fmt.append(self.start_date)
76-
fmt.append(self.interval)
68+
fmt.append(interval)
69+
nowstr = str(datetime.now().strftime('%Y%m%d-%H%M%S'))
70+
self.filename = '%s%s_%s-%s_%s%s' % (self.fileprefix, interval, start_year, start_month, nowstr, self.filesuffix)
7771
self.sql = self.job['sql'].format(*fmt)
7872

7973
def run_job(self):
@@ -171,7 +165,7 @@ def create_csv(self, result_set):
171165
logging.getLogger().debug(msg)
172166

173167

174-
def send_email(self):
168+
def send_email(self, subject):
175169
report_name = self.job["fullname"]
176170
attach_report = True
177171
if self.config["attach"].lower() == "no":
@@ -180,7 +174,7 @@ def send_email(self):
180174
filepath = os.path.join(self.working_dir, self.filename)
181175

182176
message = MIMEMultipart()
183-
message['Subject'] = '%s for %s starting %s' % (report_name, self.interval, self.start_date)
177+
message['Subject'] = subject
184178
message['From'] = self.sender
185179
message['To'] = self.recipients
186180

tests/conftest.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# pytest configuration file
2+
3+
import os
4+
import pytest
5+
import dbjobs
6+
import logging
7+
8+
9+
@pytest.fixture(scope="session")
10+
def testconfdir():
11+
"""Return the directory path to the configuration files."""
12+
config_dir = os.path.abspath(
13+
os.path.join(os.path.dirname(__file__), "..", "conf")
14+
)
15+
if not os.path.exists(config_dir):
16+
pytest.skip(
17+
"No configuration dir found. Skipping end-to-end tests."
18+
)
19+
return config_dir
20+
21+
@pytest.fixture
22+
def testdbreport(testconfdir):
23+
"""Return a DBReport object for the test configuration."""
24+
yield dbjobs.create("report", job_section="FaultReport", conf_dir=testconfdir, log_level=logging.DEBUG)

tests/test_dbreport.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import time
2+
3+
4+
def test_report(testdbreport):
5+
testdbreport.make_sql("month", "2022", "01")
6+
testdbreport.run_job()
7+
# testdbreport.send_email()
8+
9+
# assert that output file exists in expected dir and so on ...
10+

0 commit comments

Comments
 (0)