OOP And ORMs in Python
·
Python, along with many
other programming langauges, use Object-Oriented Programming (OOP). An ‘object’
is a discrete item. OOP allows for the creation of classes, which are the
generic forms of objects. For example, a ‘flight’ class is defines all the
components which describe a flight, as well as actions that a flight should be
able to take, such as adding a passenger. Similarly, a passenger class would
represent the generic idea of passenger, defined by a name and associated with
a flight, perhaps.
·
Here’s a simple example
of a Python class.
·
class Flight:
·
·
def __init__(self, origin, destination, duration):
·
self.origin = origin
·
self.destination = destination
·
self.duration = duration
o __init__ is
a ‘method’, which is a function performed on individual objects. __init__ in
particular is a special, built-in method that describes what should happen when
a flight object is created.
o Generally, methods take self as their first
argument. self refers to the object being worked with. The other three
arguments are simply the information that should be stored about a particular
flight. That information is stored as ‘properties’ inside the object, using dot
notation.
·
Here’s how the Flight class
might be used:
·
# Create flight.
·
f = Flight(origin="New York", destination="Paris", duration=540)
·
·
# Change the value of a propety.
·
f.duration += 10
·
·
# Print details about flight.
·
print(f.origin)
·
print(f.destination)
·
print(f.duration)
o Note that only flight information is passed in to Flight();
the self argument
to the __init__ method is automatically specified.
o f is
a variable of type Flight, just like a variable might be of type str or int.
·
Additional methods can be
added to the Flight class:
·
class Flight:
·
·
# assume same __init__ method
·
·
def print_info(self):
·
print(f"Flight origin: {self.origin}")
·
print(f"Flight destination: {self.destination}")
·
print(f"Flight duration: {self.duration}")
·
·
def main():
·
f1 = Flight(origin="New York", destination="Paris", duration=540)
·
f1.print_info()
o Now, this functionality of printing out flight info can be used
with any flight object that might be created. Each time, self refers
to the object that the method is being called on. In this example, that’s f1.
·
Methods can also take
additional arguments and modify properties.
·
def delay(self, amount):
·
self.duration += amount
o Note that writing methods like delay and print_info, as
well just the idea of Flight class in general, allow for abstraction. The Flight class
and all of its methods can be used in a logical and easily understood way
without needing to know or even understand how Flight may
be implemented.
·
Given a simple Passenger class…
·
class Passenger:
·
·
def __init__(self, name):
·
self.name = name
·
A more complex Flight class
can be implemented.
·
class Flight:
·
·
counter = 1
·
·
def __init__(self, origin, destination, duration):
·
·
# Keep track of id number.
·
self.id = Flight.counter
·
Flight.counter += 1
·
·
# Keep track of passengers.
·
self.passengers = []
·
·
# Details about flight.
·
self.origin = origin
·
self.destination = destination
·
self.duration = duration
·
·
def print_info(self):
·
print(f"Flight origin: {self.origin}")
·
print(f"Flight destination: {self.destination}")
·
print(f"Flight duration: {self.duration}")
·
·
print()
·
print("Passengers:")
·
for passenger in self.passengers:
·
print(f"{passenger.name}")
·
·
def add_passenger(self, p):
·
self.passengers.append(p)
·
p.flight_id = self.id
o Note that counter is defined outside of the __init__ function
and is not specific to individual flights (it’s not defined as self.counter. This
means that all flight objects can see this same counter variable, which allows
for the implementation the id property shown here. Similar to the SQL
database which had an auto-incrementing id column,
the id property
of flights will automatically incrememt as new flight objects are created.
o The passengers property of Flights is going to be a
list of Passenger objects.
o In add_passenger, p.flight_id is created,
because flight_id is not defined in the Passenger class’s __init__.
·
Here’s how the more
advanced Flight class could be used:
·
# Create flight.
·
f1 = Flight(origin="New York", destination="Paris", duration=540)
·
·
# Create passengers.
·
alice = Passenger(name="Alice")
·
bob = Passenger(name="Bob")
·
·
# Add passengers.
·
f1.add_passenger(alice)
·
f1.add_passenger(bob)
·
·
f1.print_info()
·
Object-Relational
Mapping, or ORM, allows for the combination of the OOP world of Python and the
relational database world of SQL. With ORM, Python classes, methods, and
objects become the tools for interacting with SQL databases. To do this, the
Flask-SQLAlchemy package will be used.
·
The basic setup,
inside models.py:
·
from flask_sqlalchemy import SQLAlchemy
·
·
db = SQLAlchemy()
·
·
class Flight(db.Model):
·
__tablename__ = "flights"
·
id = db.Column(db.Integer, primary_key=True)
·
origin = db.Column(db.String, nullable=False)
·
destination = db.Column(db.String, nullable=False)
·
duration = db.Column(db.Integer, nullable=False)
·
·
·
class Passenger(db.Model):
·
__tablename__ = "passengers"
·
id = db.Column(db.Integer, primary_key=True)
·
name = db.Column(db.String, nullable=False)
·
flight_id = db.Column(db.Integer,
db.ForeignKey("flights.id"), nullable=False)
o For any table inside of the database, there is one class defined
inside models.py.
o Adding db.Model in parentheses after class names indicates that these
classes ‘inherit’ from db.Model. The details of inheritance are unimportant right now; simply,
this allows for the class to have some built-in relationship with SQLAlchemy to
interact with the database.
o __tablename__ naturally
corresponds with the table name inside the database.
o Every property is defined as a db.Column, which
will become columns in the table. The arguments to db.Column are
naturally similar to those use for table creation in SQL.
o Note that flights.id is marked as a foreign key using the __tablename__ flights, not
the class name Flight.
·
Now that there’s a
defined structure for how the tables should look, they can be created inside a
Flask application.
·
import os
·
·
from flask import Flask, render_template, request
·
·
# Import table definitions.
·
from models import *
·
·
app = Flask(__name__)
·
·
# Tell Flask what SQLAlchemy databas to use.
·
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL")
·
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
·
·
# Link the Flask app with the database (no Flask app is actually
being run yet).
·
db.init_app(app)
·
·
def main():
·
# Create tables based on each table definition in `models`
·
db.create_all()
·
·
if __name__ == "__main__":
·
# Allows for command line interaction with Flask application
·
with app.app_context():
·
main()
·
db.create_all() is
the Python-Flask-SQLAlchemy’s version of the CREATE SQL
command.
·
SQL’s INSERT…
·
INSERT INTO flights
·
(origin, destination, duration)
·
VALUES ('New York', 'Paris', 540)
·
…and Python’s INSERT.
·
flight = Flight(origin="New York", destination="Paris", duration=540)
·
db.session.add(flight)
o SQlAlchemy automatically takes care of SQL transactions
with db.session.
·
SQL’s SELECT…
·
SELECT * FROM flights;
·
SELECT * FROM flights
·
WHERE origin = 'Paris';
·
SELECT * FROM flights
·
WHERE origin = 'Paris' LIMIT 1;
·
SELECT COUNT(*) FROM flights
·
WHERE origin = 'Paris';
·
SELECT * FROM flights WHERE id = 28;
·
SELECT * FROM flights
·
ORDER BY origin;
·
SELECT * FROM flights
·
ORDER by origin DESC;
·
SELECT * FROM flights
·
WHERE origin != 'Paris';
·
SELECT * FROM flights
·
WHERE origin LIKE '%a%';
·
SELECT * FROM flights
·
WHERE origin IN ('Tokyo', 'Paris');
·
SELECT * FROm flights
·
WHERE origin = "Paris"
·
AND duration > 500;
·
SELECT * FROm flights
·
WHERE origin = "Paris"
·
AND duration > 500;
·
SELECT * FROM flights JOIN passengers
·
ON flights.id = passengers.flight_id;
·
…and Python’s SELECT:
·
Flight.query.all()
·
Flight.query.fliter_by(origin="Paris").all()
·
Flight.query.filter_by(origin="Paris").first()
·
Flight.query.filter_by(origin="Paris").count()
·
Flight.query.get(28)
·
Flight.query.order_by(Flight.origin).all()
·
Flight.query.order_by(Flights.origin.desc()).all()
·
Flight.query.filter(Flight.origin != "Paris").all()
·
Flight.query.filter(Flight.origin.like("%a%")).all()
·
Flight.query.filter(Flight.origin.in_(["Tokyo", "Paris"])).all()
·
Flight.query.filter(and_(Flight.origin == "Paris", Flight.duration > 500)).all()
·
Flight.query.filter(or_(Flight.origin == "Paris", Flight.duration > 500)).all()
·
db.session.query(Flight, Passenger).filter(Flight.id == Passenger.flight_id).all()
·
SQL’s UPDATE…
·
UPDATE flights SET duration = 280
·
WHERE id = 6;
·
…and Python’s UPDATE:
·
flight = Flight.query.get(6)
·
flight.duration = 280
·
SQL’s DELETE…
·
DELETE FROM flights WHERE id = 28;
·
…and Python’s DELETE:
·
flight = Flight.query.get(28)
·
db.ksession.delete(flight)
·
Some other miscellaneous
SQL commands…
·
COMMIT;
·
·
…and their Python
parallels.
·
db.session.commit()
·
Before, when importing
data from a CSV file, SQL code had to be written directly into the Python file.
Now, SQLAlchemy can take care of that behind the scenes.
·
import csv
·
·
# Same setup code as before.
·
·
def main():
·
f = open("flights.csv")
·
reader = csv.reader(f)
·
for origin, destination, duration in reader:
·
flight = Flight(origin=origin, destination=destination, duration=duration)
·
db.session.add(flight)
·
print(f"Added flight from {origin} to {destination} lasting {duration} minutes.")
·
db.session.commit()
·
Putting it all together,
here’s the same web application from the end of Lecture 3, using SQLAlchemy.
Note that there are no raw SQL commands. The power of ORM, classes, and objects
is used to insert and select from the database.
·
from flask import Flask, render_template, request
·
from models import *
·
·
app = Flask(__name__)
·
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL")
·
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
·
db.init_app(app)
·
·
·
@app.route("/")
·
def index():
·
flights = Flight.query.all()
·
return render_template("index.html", flights=flights)
·
·
·
@app.route("/book", methods=["POST"])
·
def book():
·
"""Book a flight."""
·
·
# Get form information.
·
name = request.form.get("name")
·
try:
·
flight_id = int(request.form.get("flight_id"))
·
except ValueError:
·
return render_template("error.html", message="Invalid flight
number.")
·
·
# Make sure the flight exists.
·
flight = Flight.query.get(flight_id)
·
if flight is None:
·
return render_template("error.html", message="No such flight with that
id.")
·
·
# Add passenger.
·
passenger = Passenger(name=name, flight_id=flight_id)
·
db.session.add(passenger)
·
db.session.commit()
·
return render_template("success.html")
·
·
·
@app.route("/flights")
·
def flights():
·
"""List all flights."""
·
flights = Flight.query.all()
·
return render_template("flights.html", flights=flights)
·
·
·
@app.route("/flights/<int:flight_id>")
·
def flight(flight_id):
·
"""List details about a single
flight."""
·
·
# Make sure flight exists.
·
flight = Flight.query.get(flight_id)
·
if flight is None:
·
return render_template("error.html", message="No such flight.")
·
·
# Get all passengers.
·
passengers =
Passenger.query.filter_by(flight_id=flight_id).all()
·
return render_template("flight.html", flight=flight, passengers=passengers)
·
Because classes are flexible,
whatever additional functionality the app may need can be built into classes.
Adding passengers, for example, can be defined as a method in the Flight class
(in models.py).
·
def add_passenger(self, name):
·
p = Passenger(name=name, flight_id=self.id)
·
db.session.add(p)
·
db.session.commit()
·
Now, after verifying that
the flight exists, the all that is needed in the book function
of application.py is
the following:
·
flight.add_passenger(name)
o Now, there is on direct creation of passengers in the
application. It’s all handled by the Flight class.
·
One more powerful feature
of ORMs is the idea of relationships. SQLAlchemy relationships are an easy way
to take one table and relate it to another table, such that the each can refer
to the other. A relationship is set up with a single line, which in this case
would be added to the definition of the Flight class.
·
passengers = db.relationship("Passenger", backref="flight", lazy=True)
o passengers is
not a column, but rather just a relationship. Given a flight object, the passengers property
can be used to extract all the passenger info for that flight.
o backref creates
a relationship in the opposite direction, from Flight to Passenger.
o lazy indicates
that the information should be fetched only when it’s asked for.k
·
With these relationships
set up, the code in application.py’s flight function to list
get all passengers is extremely simplified.
·
passengers = flight.passengers
·
Once again, SQL’s SELECT…
·
SELECT * FROM passengers
·
WHERE flight_id = 1
·
SELECT * FROM flights JOIN passengers
·
ON flights.id = passengers.flight_id
·
WHERE passengers.name = 'Alice';
·
…and Python’s
relationship-powered SELECT.
·
Flight.query.get(1).passengers
·
Passenger.query.filter_by(name="Alice").first().flight
·
An Application
Programming Interface, or API, is a protocol for communication between
different web applications or different components of the same application.
These different components will want to share information with each other or
perform actions on other spaces, and APIs allow for this interaction. It is
useful, then, to have a standard language for how this communication will
occur.
·
One such language is
Javascript Object Notation (JSON), which is a simple way of representing
information in human- and computer-readable way so that it can be passed
between parts of web application.
·
Some example JSON:
·
{
·
"origin" : {
·
"city": "Tokyo",
·
"code": "HND"
·
},
·
"destination": {
·
"city": "Shanghai",
·
"code": "PVG"
·
},
·
"duration" : 185,
·
"passengers" : ["Alice", "Bob"]
·
}
o The curly braces enclose a JSON object.
o The contets of the JSON object are dvided into key-value pairs.
o origin and duration are
themselves JSON objects, which are nested in a hierarchical structure.
o passengers shows
how lists can be values.
·
Often times, the interaction
between two APIs happens through the URL, which specifics which particular
information that should be accessed. Some different levels might be:
·
/flights/
·
/flights/28/
·
/flights/28/passengers/
·
/flights/28/passengers/6/
·
Often times, there are
different ways an API can be used. For example, one might get information about
a passenger, register a new passenger, or change registration information for a
flight.
·
The HTTP request method
will correspond to the type of action that should be performed. This is simply
a convention that many APIs follow. Some HTTP methods include:
o GET :
retrieve a resource
o POST :
create a new resource
o PUT :
replace a resource
o PATCH :
update a resource
o DELETE :
delete a resource
·
The Python Requests
library allows for all these different HTTP methods to be used.
·
import requests
·
·
def main():
·
res = requests.get("https://www.google.com/")
·
print(res.text)
·
* `res` (response) is the HTTP response that
comes from submitting, in this case, a `GET` request to a URL. All the
following are also valid:
·
* `requests.post(url)`
·
* `requests.put(url)`
·
* `requests.patch(url)`
·
* `requests.delete(url)`
·
* `res.text` is the HTML content of the page
that is returned from the request.
·
To demonstrate the
potential for these requests, Fixer, a
foreign exchange rate API, will be used in the following examples.
·
Accessing the API at the
URL
http://data.fixer.io/api/latest?access_key=apikey&base=EUR&symbols=USD’
returns the following JSON:
·
{
·
"success": true,
·
"timestamp": 1519296206,
·
"base": "EUR",
·
"date": "2018-07-11",
·
"rates": {
·
"USD": 1.177482
·
}
·
}
·
This API can be accessed
in Python using the Requests library.
·
res = requests.get("http://data.fixer.io/api/latest?access_key=apikey&base=EUR&symbols=USD")
·
if res.status_code != 200:
·
raise Exception("ERROR: API request unsuccessful.")
·
data = res.json()
·
print(data)
·
* Checking the status code of the HTTP
response ensures that the API returned what is expected by the application (a
JSON formatted like the one above). As an aside, here are some common HTTP
status codes. Generally, a leading 2 indicates a successful response, while a
leading 4 indicates a failed request.
·
* `200 OK`
·
* `201 Created`
·
* `400 Bad Request`
·
* `403 Forbidden`
·
* `404 Not Found`
·
* `405 Method Not Allowed`
·
* `422 Unprocessable Entity`
·
* `res.json()` simply extracts the JSON
response and puts into the Python variable `data`.
·
The previous returned the
entire, raw JSON returned by the API. Since the format of the JSON is
consistent and known, the most relevant information can be extracted and
displayed.
·
rate = data["rates"]["USD"]
·
print(f"1 EUR is equal to {rate} USD")
·
For a little more
flexibility on what currencies are being converted, user input can be taken
like so:
·
base = input("First Currency: ")
·
other = input("Second Currency: ")
·
res = requests.get("http://data.fixer.io/api/latest",
·
params={"access_key": apikey, "base": base, "symbols": other})
o What parameters should be passed into params (in
this case, "access_key", "base" and "symbols") are
defined in the API documentation.
·
To implement an API for
the recurring example of a airline flight manager, all that needs to be done is
to define a route that returns a JSON object, just like Fixer does.
·
from flask import Flask, render_template, jsonify, request
·
·
# ... other imports, set up code, and routes ...
·
·
@app.route("/api/flights/<int:flight_id>")
·
def flight_api(flight_id):
·
"""Return details about a single
flight."""
·
·
# Make sure flight exists.
·
flight = Flight.query.get(flight_id)
·
if flight is None:
·
return jsonify({"error": "Invalid flight_id"}), 422
·
·
# Get all passengers.
·
passengers = flight.passengers
·
names = []
·
for passenger in passengers:
·
names.append(passenger.name)
·
return jsonify({
·
"origin": flight.origin,
·
"destination": flight.destination,
·
"duration": flight.duration,
·
"passengers": names
·
})
·
The route URL is clearly
marked as an API, and takes any flight ID as a parameter.
·
jsonify is
a function provided by Flask that takes in a Python dictionary and converts it
into JSON.
·
If there is no flight
found, an HTTP status code (422) is also returned with the JSON to indicate an
error has occurred.
·
Seen here again is the
readability and simplicity of relationships when retrieving passenger
information.
·
If a valid flight ID was
passed as a parameter, then a JSON object with all the flight info and a list
of passengers is returned (because no status code is specified, it is set to
200 by default).
·
With larger APIs, an
often-implemented feature is rate limiting. It is undesirable to have users
making a large number of requests that might overload the API or make it harder
for other users to access it. To restrict access, users must first obtain an
API key (a long string) which must be provided with any API request. Keys allow
for the tracking of individual users only allowing, for example, 100 requests
per hour per user.
Comments
Post a Comment