29
loading...
This website collects cookies to deliver better user experience
FROM python:3.8-alpine
COPY requirements.txt .
RUN apk update
RUN pip install -r requirements.txt
COPY app /app
RUN ls -la
WORKDIR /app
EXPOSE 5000
ENTRYPOINT [ "./start.sh" ]
Flask==2.0
gunicorn==20.1.0
pendulum==2.1.2
google-api-python-client
google-auth-oauthlib
google-auth-httplib2
oauth2client
webargs==8.0.0
version: "3.6"
services:
youtube-search:
container_name: youtube-search-service
build: .
image: python:latest
ports:
- 5000:5000
volumes:
- ./app/app.py:/app/app.py
- ./app/utils.py:/app/utils.py
- ./app/models/schema.py:/app/models/schema.py
- ./app/youtube_manager.py:/app/youtube_manager.py
- ./app/response_manager.py:/app/response_manager.py
environment:
TIMEOUT: 100
DEBUG: "true"
LOG_LEVEL: INFO
TIMEZONE: America/Santiago
DATE_FORMAT: YYYY-MM-DD HH:mm:ss
API_KEY_CREDENTIALS: ${API_KEY_CREDENTIALS}
${API_KEY_CREDENTIALS}
this variable is in a .env
file and refer to API provided by Google to connect with this API, you can get this on your Google Console Dashboard.app.py
in this one we're going to add the code shown below to consult the Youtube API and return a response to the frontend.# app.py
import json
import logging
from os import environ
from flask import Flask
from models.schema import SearchSchema
from webargs.flaskparser import use_args
from googleapiclient.errors import HttpError
from response_manager import ResponseManager
from youtube_manager import YoutubeManager
from marshmallow.exceptions import ValidationError
import utils as utils
# app.py
# Init Flask
app = Flask(__name__)
# Set logger level
LOG_LEVEL = environ.get('LOG_LEVEL', 'INFO')
logging.basicConfig(level=LOG_LEVEL)
response_manager
that contains a Class
and this helps us (as the name reveals) to manage all the responses we make to the frontend. Let's create it and later instance it in the app.py
file.# response_manager.py
import json
import logging
from os import environ
JSON_MIMETYPE='application/json'
LOG_LEVEL = environ.get('LOG_LEVEL', 'INFO')
# Set logger level
logging.basicConfig(level=LOG_LEVEL)
class ResponseManager():
def __init__(self, app) -> None:
self.app = app
self.mimetype=JSON_MIMETYPE
self.headers={'Access-Control-Allow-Origin': '*', 'Content-Type': JSON_MIMETYPE}
# list of all possible errors
self.list_status = { 400: 'fail', 422: 'fail', 500: 'error', 200:'success', 203: 'success' }
self.status_code = None
def message(self, message, status_code) -> dict:
self.status_code = status_code
try:
return self.app.response_class(
response = json.dumps({
'code': self.status_code
, 'status': self.list_status.get(self.status_code, 200) # default: 200
, 'message': message
})
, status=self.status_code
, mimetype=self.mimetype
, headers=self.headers
)
except Exception as error:
logging.error(error)
logging.exception('Something went wrong trying send message')
ResponseManager
previously created, add the next code to do it.# app.py
# Init ResponseManager
response_manager = ResponseManager(app)
# app.py
@app.route('/')
def index():
return response_manager.message('Service is Working', 200)
5000
and with the environment variable DEBUG
we enable the debug on it.if __name__ == '__main__':
app.run(debug=environ.get('DEBUG', "true"), host='0.0.0.0', port=5000)
app.py
file looks like this.import json
import logging
from os import environ
from flask import Flask
from models.schema import SearchSchema
from webargs.flaskparser import use_args
from googleapiclient.errors import HttpError
from response_manager import ResponseManager
from youtube_manager import YoutubeManager
from marshmallow.exceptions import ValidationError
import utils as utils
# Init Flask
app = Flask(__name__)
# Set logger level
LOG_LEVEL = environ.get('LOG_LEVEL', 'INFO')
logging.basicConfig(level=LOG_LEVEL)
# Init ResponseManager
response_manager = ResponseManager(app)
@app.route('/')
def index():
return response_manager.message('Service is Working', 200)
if __name__ == '__main__':
app.run(debug=environ.get('DEBUG', "true"), host='0.0.0.0', port=5000)
YoutubeManager
.# youtube_manager.py
import logging
from os import environ
from googleapiclient.discovery import build
# Set logger level
LOG_LEVEL = environ.get('LOG_LEVEL', 'INFO')
logging.basicConfig(level=LOG_LEVEL)
class YoutubeManager():
def __init__(self, api_key) -> None:
self.developer_api_key = api_key
self.youtube_service_name = "youtube"
self.youtube_api_version = "v3"
self._youtube_cli = None
def __enter__(self):
try:
logging.info('Initializing YouTube Manager')
if self._youtube_cli is None:
self._youtube_cli = build(
self.youtube_service_name
, self.youtube_api_version
, developerKey=self.developer_api_key
)
else:
logging.info('Return existent client')
except Exception as error:
logging.error(error)
else:
logging.info('Returning YouTube Manager')
return self._youtube_cli
def __exit__(self, type, value, traceback) -> None:
try:
if self._youtube_cli is not None:
self._youtube_cli = None
except Exception as error:
logging.error(error.args)
else:
logging.info('YouTube client closed')
YoutubeManager
now let's go and create all utils functions, these functions will help us prepare the response to the frontend, I made it separate to isolate each part of code on small pieces for an easy debug in case of any problem.# utils.py
# Prepare the response
def normalize_response(search_response, per_page = 100) -> dict:
return {
'prev_page': search_response.get('prevPageToken', None)
, 'next_page': search_response.get('nextPageToken', None)
, 'page_info': search_response.get('pageInfo', {'totalResults': 0, 'resultsPerPage': per_page})
, 'items': normalize_items_response(search_response.get('items', []))
}
# Prepare each item only with the required fields
def normalize_items_response(items) -> list:
list_videos = []
for item in items:
item_id = item.get('id')
item_snippet = item.get('snippet')
item_thumbnails = item_snippet.get('thumbnails')
new_item = {
'id': get_id_item(item_id)
, 'type': get_type_item(item_id.get('kind'))
, 'description': item_snippet.get('description')
, 'title': item_snippet.get('title')
, 'channel_id': item_snippet.get('channelId')
, 'channel_title': item_snippet.get('channelTitle')
, 'published_at': item_snippet.get('publishedAt')
, 'thumbnails': item_thumbnails.get('high')
}
list_videos.append(new_item)
return list_videos
# Validate & Return the type of item
def get_type_item(kind):
if kind == 'youtube#video':
return 'video'
elif kind == 'youtube#channel':
return 'channel'
else:
return 'playlist'
# Validate & Return the ID according to each type
def get_id_item(item):
if item.get('kind') == 'youtube#video':
return item.get('videoId')
elif item.get('kind') == 'youtube#channel':
return item.get('channelId')
else:
return item.get('playlistId')
app.py
.# app.py
@app.route('/search', methods=['GET'])
def search_in_youtube(args):
try:
with YoutubeManager(environ.get('API_KEY_CREDENTIALS')) as youtube_manager:
logging.info('Initializing search in Youtube API')
max_results = args.get('per_page', 100)
# Validating per_page parameter
if (args.get('per_page') is not None):
if int(args.get('per_page')) < 0:
raise ValidationError('Items per page must be greater than zero (0)')
# We do the search using the YouTube API
search_response = youtube_manager.search().list(
q=args.get('query_search')
, part='id, snippet'
, maxResults=max_results
, pageToken=args.get('page_token', None)
).execute()
response = utils.normalize_response(search_response, max_results)
return response_manager.message(response, 200)
except ValidationError as error:
logging.info(error)
return response_manager.message(error.args, 422)
# If is an HttpErrorException make send this message and log the error.
except HttpError as error:
error = json.loads(error.args[1])
logging.error(error)
return response_manager.message('Something went wrong searching in Youtube API', 500)
except Exception as error:
logging.error(error)
return response_manager.message(error.args, 500)
# app.py
# Added function to send response when there is an error with status code 422
@app.errorhandler(422)
def validation_error(err):
messages = err.data.get('messages')
if 'query' in messages:
return response_manager.message(messages.get('query'), 422)
elif 'json' in messages:
return response_manager.message(messages.get('json'), 422)
"models/schema.py"
with the code below:# models/schema.py
from marshmallow import Schema, fields
from marshmallow.exceptions import ValidationError
class SearchSchema(Schema):
per_page=fields.Integer()
query_search=fields.String(required=True)
page_token=fields.String()
@use_args(SearchSchema(), location='query')
in the app.py
file and the function will look like this.# app.py
@app.route('/search', methods=['GET'])
@use_args(SearchSchema(), location='query') # -> Added code
def search_in_youtube(args):
# search function code created before.
app.py
will looks like this:import json
import logging
from os import environ
from flask import Flask
from models.schema import SearchSchema
from webargs.flaskparser import use_args
from googleapiclient.errors import HttpError
from response_manager import ResponseManager
from youtube_manager import YoutubeManager
from marshmallow.exceptions import ValidationError
import utils as utils
# Init Flask
app = Flask(__name__)
# Set logger level
LOG_LEVEL = environ.get('LOG_LEVEL', 'INFO')
logging.basicConfig(level=LOG_LEVEL)
# Init ResponseManager
response_manager = ResponseManager(app)
# Added function to send response when there is an error with status code 422
@app.errorhandler(422)
def validation_error(err):
messages = err.data.get('messages')
if 'query' in messages:
return response_manager.message(messages.get('query'), 422)
elif 'json' in messages:
return response_manager.message(messages.get('json'), 422)
@app.route('/')
def index():
return response_manager.message('Service is Working', 200)
@app.route('/search', methods=['GET'])
@use_args(SearchSchema(), location='query')
def search_in_youtube(args):
try:
with YoutubeManager(environ.get('API_KEY_CREDENTIALS')) as youtube_manager:
logging.info('Initializing search in Youtube API')
max_results = args.get('per_page', 100)
# Validating per_page parameter
if (args.get('per_page') is not None):
if int(args.get('per_page')) < 0:
raise ValidationError('Items per page must be greater than zero (0)')
# We do the search using the YouTube API
search_response = youtube_manager.search().list(
q=args.get('query_search')
, part='id, snippet'
, maxResults=max_results
, pageToken=args.get('page_token', None)
).execute()
response = utils.normalize_response(search_response, max_results)
return response_manager.message(response, 200)
except ValidationError as error:
logging.info(error)
return response_manager.message(error.args, 422)
except HttpError as error:
error = json.loads(error.args[1])
logging.error(error)
return response_manager.message('Something went wrong searching in Youtube API', 500)
except Exception as error:
logging.error(error)
return response_manager.message(error.args, 500)
if __name__ == '__main__':
app.run(debug=environ.get('DEBUG', "true"), host='0.0.0.0', port=5000)