0

I'm new to OOP, but experienced in SQL. I'm writing a Luigi ETL task to generate reports.

I have a base class called Report, and 3 subclasses Report_Daily, Report_Weekly & Report_Monthly, which dynamically set the base class' SQL query's GROUP BY, so that the appropriate reports can be generated

#reports.py

class Report:
    report_sql = {'daily':'day', 'weekly':'firstdayofweek(day)', 'monthly':'firstdayofmonth(day)'}
    @property
    def report_type(self):
        return '' # To be instantiated by subclass
    sql = 'select '+report_sql[report_type]+' count(*) from employees group by '+report_sql[report_type]
    def run(self):
        #code that runs sql query

class ReportDaily(Report):
    report_type = 'Daily'

class ReportWeekly(Report):
    report_type = 'Weekly'

class ReportMonthly(Report):
    report_type = 'Monthly'

These 3 subclasses are then scheduled via Luigi & Chronos like

luigi --module etl.reports ReportDaily
luigi --module etl.reports ReportWeekly
luigi --module etl.reports ReportMonthly

But I'm getting 'Cannot concatenate string and property' error. I tried setting report_type as a luigi parameter, but get similar error.
What is the proper design pattern to achieve this ?

d-_-b
  • 761
  • 2
  • 11
  • 26
  • Any reason you want report_type to be a property rather than regular variable? Could you specify report_type = '' in the Report class and have that overwritten by the subclasses? – jchung May 11 '21 at 03:08

1 Answers1

1

In Python, if you do this:

class Report:
    report_sql = "bla"

report_sql will be a class variable. More info in Python docs (Class and Instance Variables). Because you want to change it in your classes it is better it be an instance variable (unique to each instance).

Also, I think the Report class needs to be an abstract base class (ABC). Check the docs for more info: abc — Abstract Base Classes. Because technically, Abstract Classes shouldn't be instantiated, my __init__ method has the abstractmethod decorator. As you can read, the ReportDaily, ReportWeekly and ReportMonthly classes call the constructor of the base class with the needed report_type.

I only keep the part related to the report type and the SQL query. This is my new code:

from abc import ABC, abstractmethod


class Report(ABC):
    @abstractmethod
    def __init__(self, report_type):
        self.report_type = report_type
        self.sql = f'select {self.report_type} count(*) ' \
                   f'from employees ' \
                   f'group by {self.report_type}'

    def run(self):
        ...


class ReportDaily(Report):
    def __init__(self):
        super().__init__(report_type='Daily')


class ReportWeekly(Report):
    def __init__(self):
        super().__init__(report_type='Weekly')


class ReportMonthly(Report):
    def __init__(self):
        super().__init__(report_type='Monthly')


daily_report = ReportDaily()
print(daily_report.sql)
weekly_report = ReportWeekly()
print(weekly_report.sql)
monthly_report = ReportMonthly()
print(monthly_report.sql)
try:
    Report(report_type="dummy")
except TypeError:
    print("an abstract class cannot be instantiated!")

output:

select Daily count(*) from employees group by Daily
select Weekly count(*) from employees group by Weekly
select Monthly count(*) from employees group by Monthly
an abstract class cannot be instantiated!

Notes:

Franco Morero
  • 549
  • 5
  • 20
  • Thanks. follow up : my base class itself derives from other classes, and somewhere in there, multiple keyword arguments are passed in to constructor. How should the derived class constructor account for it ? def __init__(self,**kwargs): super(**kwargs).__init__(report_type = 'monthly') ? – d-_-b May 11 '21 at 04:29
  • 1
    Put the `**kargs` inside `__init__` : `def __init__(self,**kwargs): super().__init__(report_type = 'monthly', **kwargs)` . More info of `super()` in: https://stackoverflow.com/questions/576169/understanding-python-super-with-init-methods – Franco Morero May 11 '21 at 11:24