Python 008 Many-to-Many — could be updated with new code

Greem
6 min readJan 16, 2024

code: https://github.com/labradorescence/p3-oo-many-to-many-lab

In this lab we will implement a many-to-many relationship between a `Author`, `Book`, and `Contract`.

$pipenv install # to create your virtual environment 
$pipenv shell #to enter the virtual environment
#pytest -x #to run your tests

-=

Book class

Create a Book class that has the following attributes: `title` (string)

class Book:
def __init__(self, title):
self.title = title
#pass #a temporary placeholder

-=

  • =

Author class

Create an Author class that has the following attributes: `name` (string)

class Author:
def __init__(self, name):
self.name = name
pass

=-

=-

Contract class: Intermediary class

Create a Contract class that has the following properties:

`author` (Author object), `book` (Book object), `date` (string), and `royalties` (int).

-The author property should be an instance of the Author class,

-while the book property should be an instance of the Book class.

-The date property should be a string that represents the date when the contract was signed,

-while the royalties property should be a number that represents the percentage of royalties that the author will receive for the book.

- All setters should `raise Exception` upon failure.

self._author

underscored attributes are intended to be treated as internal or protected. It’s a common practice to use underscores to indicate that an attribute is not meant to be accessed directly from outside the class.


class Contract:


def __init__(self, author, book, date, royalties):
self.author = author
self.book = book
self.date = date
self.royalties = royalties
#'_'as internal or protected
#not meant to be accessed directly from outside the class.


@property
def author(self):
return self._author

@author.setter
def author(self, value):
if isinstance(value, Author):
self._author = value
else:
raise Exception

@property
def book(self):
return self._book

@book.setter
def book(self, value):
if isinstance(value, Book):
self._book = value
else:
raise Exception

@property
def date(self):
return self._date

@date.setter
def date(self, value):
if isinstance(value, str):
self._date = value
else:
raise Exception

@property
def royalties(self):
return self._royalties

@royalties.setter
def royalties(self, value):
if isinstance(value, int):
self._royalties = value
else:
raise Exception

— -=

All classes should also keep track of `all` members using a class variable.

# import ipdb
class Author:
all = [] #<------ add this
def __init__(self, name):
self.name = name
Author.all.append(self)#<------ add this

class Book:
all = []#<------ add this
def __init__(self, title):
self.title = title
Book.all.append(self)#<------ add this

class Contract:
all = [] #<------ add this
def __init__(self, author, book, date, royalties):
self._author = author
self._book = book
self._date = date
self._royalties = royalties
Contract.all.append(self)#<------ add this
@property
.
.
.
.
.

— -=

check

#many_to_many.py
import ipdb

b1 = Book("A Dog's Purpose")
a1 = Author("W. Bruce Cameron")
c1 = Contract(a1, b1, "Monday", 30)
b2 = Book("The Three Body Problem")
a2 = Author("Liu Cixin")
c2 = Contract(a2, b2, "Tuesday", 20)

ipdb.set_trace()

$python3 lib/many_to_many.py

#add a new book
ipdb> Book("Kindred")

#get all books' titles
ipdb> for each_b in Book.all:
print(each_b.title)

#add a new author
ipdb> Author("Octavia E Butler")

#get all authors name
ipdb> for each_a in Author.all:
print(each_a.name)

#get all contracts
ipdb> for each_c in Contract.all:
print(each_c.book.title)

A Dog's Purpose
The Three Body Problem

#notice new book is not in the contract

— -=

The `Author` class, instance methods

The `Author` class should have the following methods:

=-

a1.contracts()

  • `contracts(self)`: This method should return a list of related contracts.
ipdb> for contract in Contract.all:
print(contract.author.name)
W. Bruce Cameron

— — =

so

#Author class 
class Author:
def __init__(self, name):
.....

def contracts(self):
return [contract for contract in Contract.all #list comprehension
if contract.author == self]

=-

check

#invoke the instance method
ipdb> a1.contracts()
[<__main__.Contract object at 0x102706070>]


#print it with more descirption
ipdb> for each_c in a1.contracts():
print(a1.name, "::::::", each_c.author.name, each_c.book.title,
each_c.date, each_c.royalties)

W. Bruce Cameron :::::: W. Bruce Cameron A Dog's Purpose Monday 30

=-

a1.books()

=-

`books(self)`: This method should return a list of related books using the

`Contract` class as an intermediary.

ipdb> for contract in a1.contracts():
print(contract.book.title )

so

#Author class
class Author:
.
.
.
def books(self): #list comprehension
return [ contract.book for contract in self.contracts() ]

check

#return a list of author's related books

ipdb> a1.books()
[<__main__.Book object at 0x1004a4160>]


#get the title
ipdb> for each_b in a1.books():
print(each_b.title)

A Dog's Purpose

=-

a1.sign_contract(b1, “Jan 16th, 2024”, 50)

a1.sign_contract(Book(“A Cat’s Purpose”), “Jan 16th, 2024”, 50)

=-

`sign_contract(book, date, royalties)`: This method should create and return a new `Contract` object between the author and the specified book with the specified date and royalties

    def sign_contract(self, book, date, royalties):
return Contract(self, book, date, royalties)

=-

check

#sign a new contract for the author 1
ipdb> a1.sign_contract(b1, "Jan 16th, 2024", 50)
<__main__.Contract object at 0x1101b4070>

#check all the contracts for author 1
ipdb> a1.contracts()
[<__main__.Contract object at 0x1051d29a0>, <__main__.Contract object at 0x1101b4070>]

#sign a new contract for the author 1 w a new book
a1.sign_contract(Book("A Cat's Purpose"), "Jan 16th, 2024", 50)
<__main__.Contract object at 0x11018c1c0>

#check all the contracts for author 1
ipdb> a1.contracts()
[<__main__.Contract object at 0x1051d29a0>, <__main__.Contract object at 0x1101b4070>, <__main__.Contract object at 0x11018c1c0>]
ipdb>

=-

a1.total_royalties()

=-

`total_royalties()`: This method should return the total amount of royalties

that the author has earned from all of their contracts.

#get royalties from the each contract
ipdb> for each_c in a1.contracts():
print(each_c.royalties)
30
50
50

#retreive them in a list
ipdb> [ each_c.royalties for each_c in a1.contracts()]
[30, 50, 50]

# get sum
ipdb> sum([ each_c.royalties for each_c in a1.contracts()])

130

so

class Author:
.
.
def total_royalties(self):
return sum([ each_c.royalties for each_c in self.contracts()])

check

#invoke the total_royalties method
ipdb> a1.total_royalties()
30

#add more contracts for this author to show sum example
ipdb> a1.sign_contract(Book("Catcher in the Rye"), "1951", 200)
<__main__.Contract object at 0x107623eb0>

#invoke the total_royalties method
ipdb> a1.total_royalties()
230

=-

`Contract` class, class method

=-

The `Contract` class should have the following methods:

  • A class method `contracts_by_date`(cls, date): This method should return all contracts that have the same date as the date passed into the method.

In object-oriented programming, we often organize related functionalities into classes. We have aContractclass. Within this class, there is a class method defined using the @classmethod decorator, named contracts_by_date.

Here’s a breakdown:

  1. Class Method:
  • class method belongs to the class itself
  • The @classmethod decorator indicates that this method belongs to the class itself rather than an instance of the class. It's like a method that acts on the class level rather than on an individual contract.

2. Method Signature:

  • The method is named contracts_by_date, and it takes two parameters: cls (conventionally representing the class itself) and date. This method is designed to retrieve contracts based on a specific date.

process to get the answer

# get each contract in the all contracts
ipdb> [contract for contract in Contract.all ]
[<__main__.Contract object at 0x10464ac10>, <__main__.Contract object at 0x104657190>, <__main__.Contract object at 0x107623eb0>]

# get each contract's date in the all contracts
ipdb> [contract.date for contract in Contract.all ]

['Monday', 'Tuesday', '1951']

# get contract that matches the date, Tuesday
ipdb> [contract for contract in Contract.all if contract.date == "
Tuesday"]
[<__main__.Contract object at 0x104657190>]

#get book title, and date for the contract that matches the date tuesdasy
ipdb> [(contract.book.title, contract.date) for contract in Contract.all
if contract.date == "Tuesday"]
[('The Three Body Problem', 'Tuesday')]

so

    @classmethod
def contracts_by_date(cls, date):
return [contract for contract in Contract.all
if contract.date == date]

check

$python3 lib/many_to_many.py

#returns a list of contracts that meet the criteria
ipdb> c1.contracts_by_date("Tuesday")
[<__main__.Contract object at 0x105232460>]

#returns details of contracts that meet the criteria
ipdb> for each_c in c1.contracts_by_date("Tuesday"):
print(each_c.book.title, each_c.author.name, each_c.date,
each_c.royalties)

The Three Body Problem Liu Cixin Tuesday 20

BIG CHECK

$pytest

b1.contracts()

FAILED module Test Book class has method contracts() that returns a list of its contracts — AttributeError: ‘Book’ object has no attribute ‘contracts’


class Book:
.....

def contracts(self):
return [ contract for contract in Contract.all
if contract.book == self ]

b1.authors()

FAILED module Test Book class has method authors() that returns a list of its authors — AttributeError: ‘Book’ object has no attribute ‘authors’

process

so

#book class 
def authors(self):
return [ contract.author for contract in self.contracts()]

check

$pytest
14 passed!!

all

# import ipdb

class Book:
all = []
def __init__(self, title):
self.title = title
Book.all.append(self)

def contracts(self):
return [ contract for contract in Contract.all if contract.book == self ]

def authors(self):
return [ contract.author for contract in self.contracts()]


class Author:
all = []
def __init__(self, name):
self.name = name
Author.all.append(self)

def contracts(self):
return [ contract for contract in Contract.all if contract.author == self]

def books(self):
return [contract.book for contract in self.contracts()]

def sign_contract(self, book, date, royalties):
return Contract(self, book, date, royalties)

def total_royalties(self):
return sum([ each_c.royalties for each_c in self.contracts()])



class Contract:
all = []
def __init__(self, author, book, date, royalties):
self.author = author
self.book = book
self.date = date
self.royalties = royalties
Contract.all.append(self)

@property
def author(self):
return self._author

@author.setter
def author(self, author):
if isinstance(author, Author):
self._author = author
else:
raise Exception

@property
def book(self):
return self._book

@book.setter
def book(self, book):
if isinstance(book, Book):
self._book = book
else:
raise Exception

@property
def date(self):
return self._date
@date.setter
def date(self, date):
if isinstance(date, str):
self._date = date
else:
raise Exception

@property
def royalties(self):
return self._royalties
@royalties.setter
def royalties(self, royalties):
if isinstance(royalties, int):
self._royalties = royalties
else:
raise Exception

@classmethod
def contracts_by_date(cls, date):
return [contract for contract in Contract.all if contract.date == date]



b1 = Book("A Dog's Purpose")
a1 = Author("W. Bruce Cameron")
c1 = Contract(a1, b1, "Monday", 30)
b2 = Book("The Three Body Problem")
a2 = Author("Liu Cixin")
c2 = Contract(a2, b2, "Tuesday", 20)

# ipdb.set_trace()

--

--