Set logging process

* Sets logging method and
  replaces basic "print" with
  logs

* Replaces /HOME/USER/.config/ directory
  with /HOME/USER/.inopy directory

* Moves config directory in new /HOME/USER/.inopy
  directory and creates a new "logs" directory

* Adapts README file
master
Alexandre Racine 2023-06-30 19:46:35 +02:00
parent f4fe6e717e
commit 7c3c922529
6 changed files with 361 additions and 246 deletions

View File

@ -11,7 +11,7 @@ Inopy is a Python application that retrieves unread articles from the [Inoreader
## Overview
It uses OAuth authentication for accessing the [API](https://www.inoreader.com/developers) and stores the authentication data in a JSON configuration file located at `/HOME/USER/.config/inopy/config.json`. The program is divided into multiple modules and functions for making API requests, parsing response data, refreshing tokens, and sending notifications.
It uses OAuth authentication for accessing the [API](https://www.inoreader.com/developers) and stores the authentication data in a JSON configuration file located at `/HOME/USER/.inopy/config/config.json`. The program is divided into multiple modules and functions for making API requests, parsing response data, refreshing tokens, sending notifications and logging the processes.
The code is organized into the following modules:
@ -20,6 +20,7 @@ The code is organized into the following modules:
- `oauth.py`: Implements the OAuth authentication flow using Flask.
- `refresh.py`: Contains a `refresh` function for refreshing OAuth access and refresh tokens and updating the configuration file.
- `notif.py`: Provides a function for sending notifications using D-Bus (only tested with Cinnamon desktop environment).
- `logs.py`: Defines logging options.
## Installation
@ -39,7 +40,7 @@ pip install requests pydbus flask waitress
## Usage
The first time `ino.py` module is run, it will check if `config.json` exists in `/HOME/USER/.config/inopy/`. If not it will prompt user for configuration details and create the `config.json` file. The file should contain the OAuth endpoint, client ID, client secret, callback URL, scope, CSRF value and home URL.
The first time `ino.py` module is run, it will check if `config.json` exists in `/HOME/USER/.inopy/config/`. If not it will prompt user for configuration details and create the `config.json` file. The file should contain the OAuth endpoint, client ID, client secret, callback URL, scope, CSRF value and home URL.
It should also contain the Inoreader API endpoints, notification labels, production status, browser path, host and port.

View File

@ -32,6 +32,11 @@
import json
import os
import logging
from logs import LogFile
# Set logs file
log_file = LogFile()
'''
================================================================
@ -51,8 +56,10 @@ import os
def get_config(config_path, config_file_path):
if os.path.exists(config_path):
logging.info(f'Found config directory at {config_path}')
if os.path.exists(config_file_path):
logging.info(f'Found config file at {config_file_path}!')
pass
else:
@ -64,7 +71,8 @@ def get_config(config_path, config_file_path):
# If the config directory doesn't exist, create it
os.mkdir(config_path)
print(f'{config_path} created!')
logging.info(f'{config_path} created!')
create_file(config_file_path)
# load config content
@ -167,7 +175,8 @@ def create_file(config_file_path):
with open(config_file_path, "w") as file:
json.dump(config, file, indent=4)
print(f"{config_file_path} created successfully!")
#print(f"{config_file_path} created successfully!")
logging.info(f'Created config file at {config_file_path}!')
'''
==============================================
@ -182,7 +191,7 @@ def config():
# Set the path of config file to
# /HOME/USER/.config/inopy/config.json
config_path = os.path.join(os.environ['HOME'], '.config/inopy')
config_path = os.path.join(os.environ['HOME'], '.inopy/config')
config_file = 'config.json'
config_file_path = os.path.join(config_path, config_file)

53
ino.py
View File

@ -25,7 +25,7 @@
It performs token refreshing process if the API request returns an unauthorized status code.
Inopy is structured into functions and modules for making API requests, parsing response data, refreshing tokens and sending notifications.
Inopy is structured into functions and modules for making API requests, parsing response data, refreshing tokens, sending notifications and logging the processes.
For more information about OAuth authentication, plase see <https://www.inoreader.com/developers/oauth>
@ -35,9 +35,14 @@
import requests
import json
import notif
import logging
from config import config
from oauth import app, run_app
from oauth import run_app
from refresh import refresh
from logs import LogFile
# Set logs file
log_file = LogFile()
'''
===========================================
@ -100,7 +105,12 @@ subscriptions = {}
categories = []
# Make API request to get unread counts
unread_response = APIrequest(unread_counts_url, bearer)
try:
unread_response = APIrequest(unread_counts_url, bearer)
except Exception as e:
logging.debug(e)
'''
===================================================
@ -138,8 +148,11 @@ unread_response = APIrequest(unread_counts_url, bearer)
============================================
'''
# Check for 403 error case
if unread_response.status_code == 403:
try:
# Check for 403 error case
if unread_response.status_code == 403:
logging.info('Token not available: starting oauth process...')
run_app()
with open(config_path) as config_file:
@ -149,8 +162,9 @@ if unread_response.status_code == 403:
unread_response = APIrequest(unread_counts_url, bearer)
# Check for 401 error case
elif unread_response.status_code == 401:
# Check for 401 error case
elif unread_response.status_code == 401:
logging.info('Token expired: starting refresh process...')
refresh(config_path, endpoint, client_id, client_secret, refresh_token)
with open(config_path) as config_file:
@ -160,12 +174,21 @@ elif unread_response.status_code == 401:
unread_response = APIrequest(unread_counts_url, bearer)
# Proceed with the code execution
elif unread_response.status_code == 200:
# Proceed with the code execution
elif unread_response.status_code == 200:
logging.info('API request ok: retrieving data...')
pass
except Exception as e:
logging.debug(e)
# Make API request to get feeds list
feeds_list_response = APIrequest(feeds_list_url, bearer)
try:
feeds_list_response = APIrequest(feeds_list_url, bearer)
except Exception as e:
logging.debug(e)
'''
=======================================
@ -255,8 +278,14 @@ for unread_id, count in unreadcounts.items():
====================================
'''
if message != "":
try:
if message != "":
notif.send_notification(summary, message)
logging.info('Notification successfully sent!')
else:
else:
logging.info('No unread articles. Notification not sent.')
pass
except Exception as e:
logging.debug(e)

41
logs.py Normal file
View File

@ -0,0 +1,41 @@
"""
=========================================================================================
Copyright © 2023 Alexandre Racine <https://alex-racine.ch>
This file is part of Inopy.
Inopy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Inopy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Inopy. If not, see <https://www.gnu.org/licenses/>.
=========================================================================================
DISCLAIMER: parts of this code and comments blocks were created
with the help of ChatGPT developped by OpenAI <https://openai.com/>
Followed by human reviewing, refactoring and fine-tuning.
=========================================================================================
This module aims to log the program processes.
=========================================================================================
"""
import os
import logging
# Setting the logs file
def LogFile():
# Determine the user's home directory
logs_dir = os.path.join(os.environ['HOME'], '.inopy/logs')
os.makedirs(logs_dir, exist_ok=True)
# Configure logging
log_file = os.path.join(logs_dir, "inopy.log")
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%m/%d/%Y %I:%M:%S %p', filename=log_file, level=logging.DEBUG)

154
oauth.py
View File

@ -34,61 +34,74 @@ import time
import json
import subprocess
import threading
import logging
from flask import Flask, request, redirect, render_template
from config import config
from waitress import serve
from logs import LogFile
'''
===================================
# Set logs file
log_file = LogFile()
def run_app():
'''
===================================
Load configuration values
Extract values from the config
dictionary
Build the URL for authorization
===================================
'''
===================================
'''
config = config()
conf = config()
endpoint = config['endpoint']
client_id = config['client_id']
client_secret = config['client_secret']
callback = config['callback']
scope = config['scope']
CSRF = config['csrf']
home_url = config['home_url']
prod_status = config['prod_status']
browser_path = config['browser_path']
host = config['host']
port = config['port']
config_file_path = config['config_file_path']
endpoint = conf['endpoint']
client_id = conf['client_id']
client_secret = conf['client_secret']
callback = conf['callback']
scope = conf['scope']
CSRF = conf['csrf']
home_url = conf['home_url']
prod_status = conf['prod_status']
browser_path = conf['browser_path']
host = conf['host']
port = conf['port']
config_file_path = conf['config_file_path']
url = 'https://www.inoreader.com/oauth2/auth?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}'.format(client_id, callback, scope, CSRF)
url = 'https://www.inoreader.com/oauth2/auth?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}'.format(client_id, callback, scope, CSRF)
'''
==========================
'''
==========================
Initiate the Flask app
==========================
'''
==========================
'''
app = Flask(__name__)
app = Flask(__name__)
'''
==================================
'''
==================================
When the home url is accessed:
* Redirect the user to the
authorization URL
==================================
'''
==================================
'''
@app.route('/')
def index():
try:
@app.route('/')
def index():
return redirect(url)
'''
=====================================================
logging.info('Redirecting to authorization URL...')
except Exception as e:
logging.debug(e)
'''
=====================================================
When the oauth-callback url is accessed:
* Get the authorization code and other
@ -114,12 +127,13 @@ def index():
* If an error parameter is present in the URL:
* Render the OAuth error template
=====================================================
'''
=====================================================
'''
@app.route('/oauth-callback')
try:
@app.route('/oauth-callback')
def oauth_callback():
def oauth_callback():
authorization_code = request.args.get('code')
csrf_check = request.args.get('state')
error_param = request.args.get('error')
@ -159,45 +173,57 @@ def oauth_callback():
json.dump(config, config_file, indent=4)
config_file.truncate()
logging.info('New token obtained successfully...')
return render_template('success.html', response=(access_token, refresh_token))
else:
if not csrf:
logging.warning('CRSF validation failed...')
return render_template('csrf-failed.html', response=(CSRF, csrf_check))
elif error:
error_content = request.args.get('error_description')
logging.debug(error_content)
return render_template('oauth-error.html', response=(error_param, error_content))
else:
pass
'''
======================================
except Exception as e:
logging.debug(e)
'''
======================================
When the shutdown url is accessed:
* Shut down the Flask server
gracefully
======================================
'''
======================================
'''
@app.route('/shutdown')
def shutdown():
try:
@app.route('/shutdown')
def shutdown():
request.environ.get('werkzeug.server.shutdown')
logging.info('Shutting down Flask server...')
return 'Close this browser to terminate the process!'
'''
======================================
except Exception as e:
logging.debug(e)
'''
======================================
Define a function to start the
Flask server using Waitress in
production mode
======================================
'''
======================================
'''
def start_server():
def start_server():
serve(app, host=host, port=port)
'''
======================================
'''
======================================
Define a function to start the
production server inside a new
thread.
@ -205,10 +231,11 @@ def start_server():
This is to ensure the server is
actually already running before
opening the web browser
======================================
'''
======================================
'''
def run_prod():
def run_prod():
logging.info('Running program in production mode...')
server_thread = threading.Thread(target=start_server)
server_thread.start()
time.sleep(2)
@ -217,8 +244,8 @@ def run_prod():
subprocess.run([browser_path, "-CreateProfile", "new_profile", "-no-remote"])
subprocess.run([browser_path, "-P", "new_profile", "-no-remote", home_url])
'''
======================================
'''
======================================
Define a function to start the
development server inside a new
thread.
@ -226,10 +253,11 @@ def run_prod():
This is to ensure the server is
actually already running before
opening the web browser
======================================
'''
======================================
'''
def run_dev():
def run_dev():
logging.info('Running program in development mode...')
server_thread = threading.Thread(target=start_server)
server_thread.start()
time.sleep(2)
@ -238,22 +266,24 @@ def run_dev():
webbrowser.open(home_url)
app.run()
'''
======================================
'''
======================================
Determine whether to run the
production or development server
based on the config
======================================
'''
def run_app():
======================================
'''
try:
if prod_status == "true":
run_prod()
else:
run_dev()
except Exception as e:
logging.debug(e)
'''
======================================
Run the application standalone

View File

@ -58,6 +58,11 @@
import requests
import json
import logging
from logs import LogFile
# Set logs file
log_file = LogFile()
def refresh(config_path, endpoint, client_id, client_secret, refresh_token):
headers = {"Content-type": "application/x-www-form-urlencoded"}
@ -72,10 +77,10 @@ def refresh(config_path, endpoint, client_id, client_secret, refresh_token):
response = requests.post(endpoint, data=payload, headers=headers)
if response.status_code == 200:
print("Request was successful.")
logging.info("Request was successful. Token was refreshed...")
else:
print("Request failed with status code:", response.status_code)
logging.debug(f'Request failed with status code: {response.status_code}')
data = json.loads(response.text)