PROJECTS NOTES HOME

Messy lifeapi dev notes

Some messy notes from Lifeapi project which is currently on hold.

Instead of throwing them out, keeping it here for future reference.

1 2024-03-05 Lifeapi has to close

1.1 WHY

This is a project that provides daily usage, but does not really provide a value.

You kind of know if you are reading or if you are sick or if you had belly ache a few days in a row or something like this.

But you log this stuff into the app. It is something you have to do on and on, day by day…

And also fetching data from external resources.. Now each of my machine has to have rescuetime installed, we must wear certain watches.. That is too much dependency. Those things might change and look, we no longer have the data and the visualizations that I might build would break.

Additional cost for electricity for magic mirror (if I would have one)

Additional costs running the GCP instance.

Also it's sad you can not take metrics from garmin app.

But the most important is - I have proved myself that I can do it, I have proved that it's possible, now time to move to another project.

Kind of better to just live life rather than trying to optimize it, like that quote.

1.2 THINGS LEARNED

  • How to fetch from an API
  • Cronjobs
  • GCP hosting
  • Make it simple from the beginning for the user to use the app(should have added email reminders or smth, or create an app)
  • Magicmirror attitude, built having this in mind
  • How to work with backups

1.3 STEPS

  • Make lifeapi repo private
  • Stop the cronjobs
  • Close gcp instance, cancel
  • Remove card payment
  • Lifeapi - CLOSED 2024-03-05

2 How to’s

2.1 lifeapi ip change procedure

When the server is turned off and then turned on again - the IP of it changes.

might have to fix the ssh keys like mentioned here

Then will have to login to the server over putty

change the IP in nginx config file

change the allowed hosts file in .env_prod file

restart the services

login with username and pass, if can't find - check previous ip:

first - 34.88.9.236 second - 34.88.54.183 [2024-01-28 Sun] - 35.228.228.106

or go to "pasiukai" file and find the credentials here : "# lifeapi app"

show Julyte how to use it, the new ip.

3 Intro

Dates in data_table were messed up. Installed updates on the server. Upgrade got stuck on gcp-cli package. Attempted to fix it unsuccessfully. Restarted the server - lifeapi does not start any longer. It's [2024-01-09 Tue] and lifeapi still does not work. Not cool. Thought it would be smart to dockerize the app so it would be simplier to launch it again in the future. Otherwise now I would have to do all the installation steps again.

4 Inspiration

5 Try to do the installation steps for a new gcp instance

First try this, then dockerize in the free time maybe.

6 Creating a separate branch for this

7 Creating a test docker container and running it locally

  FROM python:3.8-slim-buster

  WORKDIR /app

  COPY requirements.txt requirements.txt

  RUN pip3 install -r requirements.txt

  COPY . .
n
  CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]
docker build -t django-docker-starter .
docker run -p 8000:8000 django-docker-starter

8 Create a test docker container and run it in GCP over gcp shell

9 Run the locally created container in GCP

  • Enable Artifact Registry API service. Sorry, not this, but Google Container Registry API

Container Registry is deprecated. After May 15, 2024. Artifact Registry will host images for the gcr.io domain in projects without previous Container Registry usage. If you use Container Registry, learn about the deprecation. To get started with managing containers on Google Cloud, use Artifact Registry.

Starting January 8, 2024(today is [2024-01-09 Tue]), if your organization has not previously used Container Registry, new gcr.io repositories will be hosted on Artifact Registry by default. For more information on this change, see gcr.io hosted on Artifact Registry.

  • So YEAH, sorry, Enable Artifact Registry API service and not =Google

Container Registry API= :)

  • Download and install google cloud SDK on your windows machine from here - https://cloud.google.com/sdk/docs/install
  • try run it from search menu by writing SDK (should add sdk to path somehow not to use this workaround)
  • check if it's installed with gcloud command in cmd/powershell
  • login to your gcp account with gcloud auth login
  • gcloud config get-value project
  • Try to list all your services by gcloud services list. You should see containerregistry.googleapis.com Container Registry API in it
  • You should have a docker container already in your docker desktop
  • Tag your container with GCP stuff(over SDK)
    • docker tag django-docker-starter gcr.io/lifeapi-392202/django-docker-starter
    • Explanations:
      • gcr.io - default
      • lifeapi-392202 - this is the ID of the Lifeapi project in GCP
      • /django-docker-starter - should be the same as your image
  • After the container has been created, we can push it to GCP registry
    • docker push gcr.io/lifeapi-392202/django-docker-starter
  • If you get an error saying

    unauthorized: You don't have the needed permissions to perform this operation, and you may have invalid credentials. To authenticate your request, follow the steps in: https://cloud.google.com/container-registry/docs/advanced-authentication

    then run this command - gcloud auth configure-docker and click y.

  • try to push again, image should now be in here - https://console.cloud.google.com/gcr/images
  • Copy image url, f.x gcr.io/lifeapi-392202/django-docker-starter
  • Go to Cloud Run - https://console.cloud.google.com/run
  • Click Create service
  • choose the image
  • change port to 8000
  • tick "Allow unauthenticated invocations"

restarting lifeapi services

# 1
systemctl stop nginx
# 2
systemctl stop gunicorn.service
# 3
systemctl stop gunicorn.socket

# TURN THEM ON AGAIN

# 1
systemctl start gunicorn.socket
# 2
systemctl start gunicorn.service
# 3
systemctl start nginx

local development on a new machine

  • clone the repo
  • create venv
  • install packages from requirements.txt
  • copy .env_example and name it .env_dev
  • python manage.py createsuperuser --settings=settings.development (arvy pass)
  • python manage.py makemigrations --settings=settings.development
  • python manage.py migrate --settings=settings.development
  • python manage.py runserver --settings=settings.development
  • visit development server at http://127.0.0.1:8000/
  • DB exist?
  • Can add entries?

10 step by step setup

10.1 Initial deployment

10.1.1 step by step deployment

  1. ssh to the GCP server
  2. winscp to GCP server

    add private key - connect

    private key ends in .pem? store that private key in such location, in wsl for example:

    \\wsl.localhost\Ubuntu\home\arvy\arvydas_privatekey_gcp.pem

    the file must remain there, otherwise the connection will not work

    https://winscp.net/eng/docs/guide_google_compute_engine

  3. setup virtual env/server modifications

    sudo su - apt update apt upgrade apt install git mkdir /opt/app cd /opt/app git clone https://github.com/arvydasg/lifeapi.git apt install python3-venv python3-pip python3-dev libpq-dev postgresql postgresql-contrib nginx curl

    inspiration for deployment - - https://www.youtube.com/watch?v=bD75adrlkes&ab_channel=TheCodrammers

    python3 -m venv venv source /venv/bin/activate

    pradedu ties situ commit - 7904e80430b7b0f54c869c3e4366b0eb02d2e2c9 (Merge pull request #4 from arvydasg/DEV …) - https://github.com/arvydasg/lifeapi/commit/7904e80430b7b0f54c869c3e4366b0eb02d2e2c9

    python manage.py migrate –settings=settings.production python manage.py createsuperuser –settings=settings.production collectstatic

    python manage.py collectstatic –settings=settings.production then git ignore collectstatic dir which is "staticfiles-cdn/"

    git pull origin master # on the server open 8000 port in server

    apt install ufw ufw allow 8000 allow 8000 on gcp

    3 dots on server → view network details → default(under network) → firewalls → add firewall rule

    how it looks before:

    pictures how some firewall configuration was done in gcp

    cd /opt/app/lifeapi touch .env_prod vim .env_prod

    ALLOWED_HOSTS=34.88.9.23 python manage.py runserver –settings=settings.production 0.0.0.0:8000

  4. Okay why I am spending so much time on gunicorn and nginx

    I was like dude if I can just run django in the terminal.. why bother.

    same like with the db. Why bother with postgresql if you can use sqlite and why bother with nginx/gunicorn if you can use simle process in systemd or just run in the terminal.

    Seems like there are some benefits… but still thinking about the simple solution. Its MVP after all dude!!!!

    Using Gunicorn and Nginx in combination with Django offers several benefits over running Django in the terminal. Here are some advantages:

    Improved performance and scalability: Gunicorn is a production-ready web server that can handle multiple concurrent requests efficiently, improving the overall performance of your Django application. It utilizes a pre-fork worker model and supports multiple worker processes, allowing it to handle high traffic loads. Nginx acts as a reverse proxy server, offloading some of the processing tasks from Gunicorn and efficiently serving static files, resulting in better scalability.

    Load balancing and high availability: By using Gunicorn and Nginx together, you can set up multiple Gunicorn worker processes and distribute incoming requests among them. Nginx can act as a load balancer, evenly distributing the requests across the available Gunicorn workers. This setup improves your application's availability and can handle more traffic without overwhelming a single server instance.

    Handling concurrent connections: Gunicorn's ability to handle multiple concurrent connections makes it more suitable for production environments compared to running Django in the terminal. It allows your application to serve multiple requests simultaneously, providing a better experience for your users.

    Separation of concerns: Nginx and Gunicorn serve different purposes in a typical Django deployment setup. Nginx specializes in serving static files, handling SSL termination, and acting as a reverse proxy server. On the other hand, Gunicorn focuses on running the Django application and managing the dynamic content. This separation allows each component to perform its task efficiently and enables easier troubleshooting and maintenance.

    Regarding the issue with displaying the CSS of the admin panel when using Nginx and Gunicorn, this problem is typically related to the configuration of Nginx. You need to ensure that Nginx is configured to correctly serve static files and that the appropriate static file directories are specified. By properly configuring Nginx, you should be able to resolve this issue and have the admin panel CSS displayed correctly.

    Gunicorn Translates the client requests from http to our python calls that our application can process

    webserver gateway interface module - wsgi

    pip install gunicorn (ADD THIS TO REQUIREMENTS.TXT)

    gunicorn –bind 0.0.0.0:8000 project_name.wsgi

    gunicorn –env DJANGO_SETTINGS_MODULE=settings.production –bind 0.0.0.0:8000 lifeapi_project.wsgi

    gunicorn.socket

    vim /etc/systemd/system/gunicorn.socket

    [Unit] Description=gunicorn.socket

    [Socket] ListenStream=/run/gunicorn.sock

    [Install] WantedBy=sockets.target gunicorn.service #file name should math. Can see that both are gunicorn.

    vim /etc/systemd/system/gunicorn.service

    [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target

    [Service] User=root Group=www-data WorkingDirectory=/opt/app/lifeapi ExecStart=/opt/app/lifeapi/venv/bin/gunicorn \ –access-logfile - \ –workers 3 \ –bind unix:/run/gunicorn.sock \ lifeapi_project.wsgi:application \ –env DJANGO_SETTINGS_MODULE=settings.production

    [Install] WantedBy=multi-user.target systemctl start gunicorn.socket

    systemctl enable gunicorn.socket

    When a connection is made to gunicorn.socket systemd will automatically will start systemd.service to handle it.

    systemctl status gunicorn.socket - should be active

    systemctl status gunicorn.service- should be inactive (dead), since it did nto receive any connections

    test activation mechanism:

    curl –unix-socket /run/gunicorn.sock localhost

    Should see some html outputted. For the dude in the video it was full html, for me some html and 400 errors.

    systemctl status gunicorn.service - now should be active

    nginx vim /etc/nginx/sites-available/lifeapi

    server { listen 80; server_name 34.88.9.236; location = favicon.ico { access_log off; log_not_found off; } location /static { autoindex on; alias opt/app/lifeapi/staticfiles-cdn; } location / { include proxy_params; proxy_pass http://unix/run/gunicorn.sock; }

    } sudo ln -s etc/nginx/sites-available/lifeapi /etc/nginx/sites-enabled

    check any syntax errors:

    nginx -t

    systemctl restart nginx

    open firewall for normal traffic on the port 80

    ufw allow ‘Nginx Full’

    We no longer need access to the development server (8000), so we can remove this firewall rule:

    ufw delete allow 8000

    go to the 34.88.9.236 without any ports. Should work.

  5. Configuring static files

    this video inspiration - 74 - Static Files in Development - Python & Django 3.2 Tutorial Series

    global styles are better if wanting to override all the app styles in one place. better for the future

    can override admin styles also and other django stylesheets

    remember to turn off caching in network panel browser

    debug on - takes from static folder debug off - collecstatic need

    in prod then do:

    python manage.py collectstatic --settings=settings.production
    # 126 static files copied to '/home/arvy/src/lifeapi/staticfiles-cdn'.
    

    Ok we have stored mine and django’s static files in one location, but they are not used by anything now. When debug is true - django takes the static files from usual places, where we set them.

    But if we are in production and we ant them to be server, we need to do that with whitenoise.

    pip install whitenoise
    

    add middleware like in WhiteNoise 6.5.0 documentation . After the security one. At the top.

    MIDDLEWARE = [
        # ...
        "django.middleware.security.SecurityMiddleware",
        "whitenoise.middleware.WhiteNoiseMiddleware",
        # ...
    ]
    
    pip freeze > requirements.txt
    

    Run the server again. Even with debug = false static files for admin panel should be displayed, as well as your global styles.

  6. Running the server
    python manage.py runserver --settings=settings.production 0.0.0.0:8000
    python manage.py runserver --settings=settings.production
    python manage.py collectstatic --settings=settings.production
    

    in dev:

    • change branch to dev
    • create .env_dev file next to manage.py
    • add this:

      ALLOWED_HOSTS=127.0.0.1,localhost
      DEBUG=1
      
    • python manage.py makemigrations –settings=settings.dev
    • python manage.py migrate –settings=settings.dev
    • python manage.py createsuperuser –settings=settings.dev (root pass)
    • python manage.py collectstatic –settings=settings.dev
    • python manage.py runserver –settings=settings.dev

    or simply reuse the prod DB

  7. Restarting the services

    Link to the instructions - Restarting the services

  8. Source venv on each login to root
    sudo su -
    vim ~/.bashrc
    # add the following line
    source /opt/app/lifeapi/venv/bin/activate
    :wq
    
    source ~/.bashrc
    
  9. Cron job to fetch data automatically

    set the correct server time

    cd /opt/app/lifeapi
    chmod +x weather_job_prod.py
    crontab -e
    0 17 * * * /opt/app/lifeapi/venv/bin/python /opt/app/lifeapi/weather_job_prod.py >> /opt/app/lifeapi/weather_job_prod.log 2>&1
    

    This cron job now should run every day at 17:00.

10.1.2 deployment to gcp

Attempt2 - bare metal Firewall config - https://console.cloud.google.com/networking/firewalls/list?project=lifeapi-392202 SSH key stuff - https://www.youtube.com/watch?v=fmh94mNQHQc&ab_channel=storagefreak Logs - https://console.cloud.google.com/logs/ Attempt1 - deploy a django solution Created a project Linked billing account Set limits for billing 50 dolcu

Enabling required API’s Enable required APIs The following APIs are required to deploy a VM product from Marketplace Compute Engine API Not enabled Cloud Deployment Manager V2 API Not enabled Cloud Runtime Configuration API Not enabled Software installed Software Operating systemDebian(11.6) SoftwareApache2(2.4.56) Django(4.1.7) Git(2.30.2) MySQL-Client(8.0.32) MySQL-Community-Client(8.0.32) MySQL-Community-Server(8.0.32) MySQL-Server(8.0.32) Conclusion The build failed. For some reason. Uck it. Will make it myself better. Just because it installs django and git for me I should use a premade component and not know exactly what’s hiding under the hood? No thanks.

10.2 Second attempt

10.2.1 The plan

  • create new instance, make it run
  • reset the old instance to the beginning, make it run(to keep the same IP address)
  • Dockerize the app

10.2.2 Creating a new GCP instance

Before clicking "Create", I copied the equivalent code.

Don't choose e2-micro instance, since it will not be able to install needed packages during sudo apt upgrade - https://serverfault.com/questions/1134676/apt-upgrade-y-command-stuck-on-preparing-to-unpack-google-cloud-cli-436-0

  1. Code for creating instance

    Can look at this code and know what settings I have chosen.

    gcloud compute instances create instance-2 \
        --project=lifeapi-392202 \
        --zone=us-central1-a \
        --machine-type=e2-micro \
        --network-interface=network-tier=PREMIUM,stack-type=IPV4_ONLY,subnet=default \
        --maintenance-policy=MIGRATE \
        --provisioning-model=STANDARD \
        --service-account=46844210845-compute@developer.gserviceaccount.com \
        --scopes=https://www.googleapis.com/auth/devstorage.read_only,https://www.googleapis.com/auth/logging.write,https://www.googleapis.com/auth/monitoring.write,https://www.googleapis.com/auth/servicecontrol,https://www.googleapis.com/auth/service.management.readonly,https://www.googleapis.com/auth/trace.append \
        --tags=http-server,https-server,lb-health-check \
        --create-disk=auto-delete=yes,boot=yes,device-name=instance-2,image=projects/debian-cloud/global/images/debian-11-bullseye-v20231212,mode=rw,size=10,type=projects/lifeapi-392202/zones/us-central1-a/diskTypes/pd-standard \
        --no-shielded-secure-boot \
        --shielded-vtpm \
        --shielded-integrity-monitoring \
        --labels=goog-ec-src=vm_add-gcloud \
        --reservation-affinity=any
    
  2. Pricing

    Basically it's a e2-micro instance

    Monthly estimate

    US$6.51 That's about US$0.01 hourly

    Pay for what you use: No upfront costs and per-second billing

    Item Monthly estimate 2 vCPU + 1 GB memory US$6.11 10 GB standard persistent disk US$0.40 Total US$6.51

10.2.3 Connect to the machine over gcp console

connect over the provided default console first. should log you in with arvydas_gaspa username.

10.2.4 Connect over ssh

  1. ssh key info for instace2 (everything is no longer relevant here)

    fingerprint

    ssh-rsa 2048 SHA256:9gdAeVoX567uowDrkLCSOFVGlziSlKnSbpZDVLLdHdU

    key comment(username)

    arvydas

    public_key

    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCufZnGwCFbJyYGJtRULgEBf1qDzb8jsCFoArSd8AkCUr9mAsEbHwvuxPKQ84w3BiIeizS/Y7w168ZHcaUheWAG/pSPYxhgGU6QknW5PAVVCHPIxiXb6p6LhUbrluahcE5Cq5+HYhFZXQQ+pftI7fG/OGvBAG6f9FoxuxFaUJMhvKrQ1DG1j65I6U1UMfWVdOtyqZTw0qMpXTMjgd3rKy5bTyxEqIXH+9PL6pmSCckQZOPXXeouAqQMBRQuoxKpsG28ctTgKbjDOMoyytW9YmAAHFzxoP9tLViQMHkScWBtPKuud95IyTEdgNbwjnjUD8SCfNbjBGFITFOdURgVQwgx arvydas

    private_key

    PuTTY-User-Key-File-3: ssh-rsa Encryption: none Comment: arvydas Public-Lines: 6 AAAAB3NzaC1yc2EAAAADAQABAAABAQCufZnGwCFbJyYGJtRULgEBf1qDzb8jsCFo ArSd8AkCUr9mAsEbHwvuxPKQ84w3BiIeizS/Y7w168ZHcaUheWAG/pSPYxhgGU6Q knW5PAVVCHPIxiXb6p6LhUbrluahcE5Cq5+HYhFZXQQ+pftI7fG/OGvBAG6f9Fox uxFaUJMhvKrQ1DG1j65I6U1UMfWVdOtyqZTw0qMpXTMjgd3rKy5bTyxEqIXH+9PL 6pmSCckQZOPXXeouAqQMBRQuoxKpsG28ctTgKbjDOMoyytW9YmAAHFzxoP9tLViQ MHkScWBtPKuud95IyTEdgNbwjnjUD8SCfNbjBGFITFOdURgVQwgx Private-Lines: 14 AAABAFUBPUP034sflEeU7QWhb74CA9+IASDqsiuQfdsfT9RA6ZtRpi+HPXHxolX5 QAqiQ0br/CNs/AistuihNZgMIDroFQmRdhOC4KJPp2g5FEPrnTRnS5RKRTilEfq9 hdeJ9aZHI615mggV53Z5t+Q8fvPwEZZxlnL4QGRPxNFhxXu+NQWItmZ2Omkg3st7 8oJhTgi1WuYvVnZcIzeNJhITf1pYdrOat4tFAL7J4QjMVIFa+KpKuK3Fa4DKY0XD r+KAZuigU3vFdQjdMRvi27oJqb1qnyL1G1Z2iR3cjYWFM3KFT7mewQ7X+FERDhBp APjQ9ac4NieojYoQNtrvL00uHX0AAACBAPIz8KqgrUW1OEbr1XH9PrTdmxPCgZEp mAUfETiqYfDvhBKGzJNxekS9nHSUxjVQXWWDiXJ6KePCPu0Y1H/oKcX4ntxin5Wk 8CljbfbPHR+dvFC3jV0iGv8Gdctq8bp+eEp4ZCTU3YDWgO5tjeup3TarpooPFv2Y LwMZprbNZoyvAAAAgQC4bjVa1+BPCSpNZAiYOyUbXCv5mGWTbstyB+7dTqEOjCHy QEHdrtK8YoxmYY3ItJdjuoqKB0Souo9pSn7Vi/xN75DfapnSkF/JA1qIV5BHyuQG P6OyPLAY08PGqapxKqI0LEyFQ9WxcJBAoxUyCdQDvysO979Snr2oLVcdvV2xHwAA AIEA1tfFjfPQCHPzW40hhB/SA6l96lVRKykAtNQoDwNz7pv6Arg662yY6A8+RHRJ FuWeBwlmg/BHgAW40RSOYIkw95CFXqOF7YxH7XgnR1RDcMwyJKoJ71lJLKFOOJze RVLnm1zeO/BSSS2lKXEGEo6weH5ur4sNLYLi1aBKPTYAJMw= Private-MAC: 89ee5eb4d7113171bf86a39f453364f288e910bac34081acf1b4c117bf01a8f3

    cronjobs

10.3 weather fetch cron

0 18 * * * /opt/app/lifeapi/venv/bin/python /opt/app/lifeapi/weather_job_prod.py >> /opt/app/lifeapi/weather_job_prod.log 2>&1

10.4 rescuetime fetch

making the cronjob a bit later, like at 5am, because linux "date" command shows one time and python's datetime.now() shows a different time. Can make it equal with pytz, but no matter for now, lets see if it works with 5am.

10.5 make backup push to git cron

0 0 * * * /opt/app/lifeapi_db_backups/backup_prd.sh >> /opt/app/lifeapi_db_backups/db_backups.log 2>&1

rescuetime api implementation https://github.com/arvydasg/lifeapi/pull/65

go to the site, get api key for each person

AG api key is - xxxxxxxxxxxxx

you can then make such queries - https://www.rescuetime.com/anapi/daily_summary_feed?key=B63fYDJzgZhrzaNAbRXRgMoQ1Qxowu3iHU3Ukrpw

Then go to insomnia, add the key with API key auth, then you can make such queries then - https://www.rescuetime.com/anapi/daily_summary_feed

here are some more examples - https://www.rescuetime.com/anapi/data?&perspective=interval&restrict_kind=productivity&interval=hour&restrict_begin=2023-10-10&restrict_end=2023-10-13&format=csv https://www.rescuetime.com/anapi/data?&perspective=rank&restrict_kind=overview&restrict_begin=2023-10-12&restrict_end=2023-10-13&format=csv

sadly you can not extract day values from daily summary, so we will have to do it like such:

in this example I have my api keys in env variables.

import requests
import django
import os

# Set the DJANGO_SETTINGS_MODULE environment variable
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.development")
# Configure Django settings
django.setup()

# Your RescueTime API key
api_key_ag = os.getenv("RESCUETIME_API_KEY_AG")
api_key_js = os.getenv("RESCUETIME_API_KEY_JS")

# Define the API endpoint URL
api_url = f"https://www.rescuetime.com/anapi/daily_summary_feed?key={api_key_ag}"

# Make the API request
response = requests.get(api_url)

if response.status_code == 200:
    data = response.json()

    # Define the date you want to filter for
    target_date = "2023-10-14"

    # Filter the data for the specific date
    # here we get a big json file, so this list comperhension takes what we need
    filtered_data = [entry for entry in data if entry['date'] == target_date] # list comprehension

    if filtered_data:
# Process the filtered data as needed
for entry in filtered_data:
    print(f"Date: {entry['date']}") # we're accessing the 'date' key in the dictionary called 'entry'
    print(f"Total hours: {entry['total_hours']}")
    print(f"All productive hours: {entry['all_productive_hours']}")
    print(f"All distracting hours: {entry['all_distracting_hours']}")
    else:
print(f"No data found for {target_date}")
else:
    print(f"Failed to retrieve data. Status code: {response.status_code}")

but can also do it like such in view:

def rescuetime_app_dsf(request):
    # take the api key from environment variables
    rescuetime_api_key_ag = os.getenv("RESCUETIME_API_KEY_AG")

    # Define the URL
    url = f'https://www.rescuetime.com/anapi/daily_summary_feed?key={rescuetime_api_key_ag}'
    url2 = f'https://www.rescuetime.com/anapi/data?key={rescuetime_api_key_ag}&perspective=rank&restrict_kind=overview&restrict_begin=2023-10-10&restrict_end=2023-10-10&format=json'

    try:
# Make an HTTP GET request to the URL
response = requests.get(url)
response2 = requests.get(url2)

# Check if the request was successful (status code 200)
if response.status_code == 200:
    # Parse the JSON response
    data = response.json()
    data2 = response2.json()

    # Pass the data to the template for rendering

    context = {
'data': data,
'data2': data2
    }

    return render(request, 'rescuetime_app_dsf.html', context)
else:
    # Handle any other status code (e.g., display an error)
    return render(request, 'rescuetime_app_error.html', {'error_message': 'Failed to fetch data'})
    except Exception as e:
# Handle any exceptions (e.g., network issues, JSON parsing errors)
return render(request, 'rescuetime_app_error.html', {'error_message': str(e)})

10.6 daylt api implementation

Vardadieniai bei dienos informacija Tavo svetainėje!- https://day.lt/dienos_info_paaiskinimai.html

Nothing to explain, its just too easy to implement. If it dies - it dies. If it runs - it runs.

10.7 implementing strava api

10.8 applying the change from dev to PRD

10.8.1 Check services status

systemctl status gunicorn.socket
systemctl status gunicorn.service
systemctl status nginx

10.8.2 Fetch changes

check if there are any changes upstream(on github repo on the web)

git fetch

Push changes to DEV

Github site make pull request to master

login to gcp server

login as root user

git pull origin master on gcp

if some small change, maybe systemctl restart nginx is enough

if change in css - need to do python manage.py collectstatic –settings=settings.production

if change in some templates - need to restart ir gunicorn systemctl restart gunicorn.service (gal ir socket?)

10.9 update master

To login to MASTER branch on GCP - username "arvydas", ssh setup, so no need for password

  1. Login as root with - sudo su - . Should already be venv since bashrc is made
  2. git status
  3. cd opt/app/lifeapi
  4. gipt fetch
  5. git pull origin master
  6. python manage.py collectstatic –settings=settings.production
  7. systemctl restart nginx
  8. systemctl restart gunicorn.service

10.10 git repo change remote

krc buvo taip. as parsiputes repo buvau su https. paskui pasetupinau ssh keys serveryje del lifeapi_db_backup. Su ssh keys viskas veikia toje repo, bet nustojo veikti lifeapi. Tai teko daryti situos zingsniu. Aciu dievui, kad pavyko sitaip, nereikejo reclone ir rebuild stuff…

It seems like you've set up SSH keys for your GitHub account but are still trying to access a repository using HTTPS, and you're encountering authentication issues. You don't need to re-clone the repository with SSH; you can update the remote URL for your existing repository to use SSH. Here's how you can do it:

Open a terminal and navigate to your local repository.

Check the current remote URL using the following command:

git remote -v

You'll see the remote URL for your repository, which should currently be in HTTPS format.

To update the remote URL to use SSH, use the following command (replace username/repo with your actual repository path):

git remote set-url origin git@github.com:username/repo.git

After updating the remote URL, you can verify that it's using SSH by running git remote -v again. It should now show the SSH URL.

Try performing any Git operation (e.g., git pull, git push, etc.) to confirm that you're no longer prompted for a username and password. data table creation commit - ba650b7

Created data table · arvydasg/lifeapi@ba650b7 - https://github.com/arvydasg/lifeapi/commit/ba650b7b80596689d196be3a648904d9d63bb41f

This html template seems a bit complicated:

{% extends 'base.html' %}

{% block content %}
    <h2>Data Table</h2>
    <div class="table-responsive">
<table class="table table-striped">
    <thead>
<tr>
    <th>Date</th>
    <th>Temperature</th>
    {% for question in questions %}
<th>{{question.description}}</th>
    {% endfor %}
</tr>
    </thead>
    <tbody>
{% for weather_entry in weather_entries|dictsortreversed:'date' %}
    <tr>
    <td>{{ weather_entry.date|date:"Y-m-d" }}</td>
    <td>{{ weather_entry.temperature }}</td>
{% for question in questions %}
    {% for answer in question.answer_set.all %}
{% if answer.date_added.date == weather_entry.date %}
    <td>{{ answer.answer }}</td>
{% endif %}
    {% endfor %}
{% endfor %}
    </tr>
{% endfor %}
    </tbody>
</table>
    </div>
{% endblock %}

Here is an explanation:

The nested loop you mentioned is used to populate the table rows (<tr>) with data from the weather_entries and questions. Let's break down the loop step by step:

{% for weather_entry in weather_entries %}: This loop iterates over each weather_entry object in the weather_entries queryset. It represents each row in the table.

<tr>: Starts a new table row for each weather_entry.

<td>{{ weather_entry.date }}</td>: Displays the date value of the current weather_entry object in a table cell. This corresponds to the "Date" column of the table.

<td>{{ weather_entry.temperature }}</td>: Displays the temperature value of the current weather_entry object in a table cell. This corresponds to the "Temperature" column of the table.

{% for question in questions %}: This loop iterates over each question object in the questions queryset. It represents the columns for the questions in the table.

{% for answer in question.answer_set.all %}: This loop iterates over each answer object related to the current question object. It retrieves all answers associated with the current question.

{% if answer.date_added.date == weather_entry.date %}: This conditional statement checks if the date_added value of the current answer object is equal to the date value of the current weather_entry object. It ensures that the answer is associated with the correct date and should be displayed in the table.

<td>{{ answer.answer }}</td>: If the condition is met, it displays the answer value of the current answer object in a table cell. This corresponds to the answer for the specific question and date.

By nesting the loops, the code checks each question and its associated answer objects for each weather_entry, allowing the table to display the relevant answers for each question and date.

10.11 user question answer implementation

10.11.1 First created the models:

from django.db import models
from django.utils import timezone

# Create your models here.

class Question(models.Model):
    description = models.TextField()

class Answer(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    answer = models.CharField(max_length=255, blank=False)
    date_added = models.DateTimeField(default=timezone.now)

10.11.2 Then the views:

from django.shortcuts import render, redirect
from .models import Question, Answer
from django.http import HttpResponse
from django.utils import timezone
from datetime import date
from django.contrib import messages
from .forms import QuestionForm


def quiz_app_home(request):
    return render(request, 'quiz_app_home.html')


def quiz_questions(request):
    '''View to display all the questions'''
    questions = Question.objects.all()
    context = {'questions': questions}
    return render(request, 'quiz_app_questions.html', context)


def quiz_add_question(request):
    '''View to add a posibility to add new questions'''
    if request.method == 'POST':
form = QuestionForm(request.POST)
if form.is_valid():
    form.save()
    return redirect('quiz_questions')
    else:
form = QuestionForm()

    context = {'form': form}
    return render(request, 'quiz_app_add_question.html', context)


def quiz_start(request):
    '''View to add a posibility to add new questions'''
    if request.method == 'POST':
# Check if the user clicked the "Start Quiz" button
if 'start_quiz' in request.POST:
    # Check if entries for today already exist
    today = date.today()
    if Answer.objects.filter(date_added__date=today).exists():
messages.warning(request, "You have already answered the quiz for today.")
    else:
# Retrieve the first question from the database
first_question = Question.objects.first()
context = {'question': first_question}

return render(request, 'quiz_app_question.html', context)

# Check if the user clicked the "Delete Answers" button
elif 'delete_answers' in request.POST:
    today = date.today()
    Answer.objects.filter(date_added__date=today).delete()
    messages.success(request, "Your answers for today have been deleted.")

    # Retrieve any flash messages and pass them to the template context
    messages_to_display = messages.get_messages(request)
    context = {'messages': messages_to_display}

    return render(request, 'quiz_app_ready.html', context)


def quiz_question(request, question_id):
    if request.method == 'POST':
question_id = int(request.POST.get('question_id'))
answer_text = request.POST.get('answer')

# Save the answer to the database
answer = Answer(question_id=question_id, answer=answer_text)
answer.save()

# Redirect to the next question or finish the quiz if all questions are answered
next_question_id = question_id + 1
if next_question_id > Question.objects.count():
    return redirect('quiz_summary')  # Redirect to the quiz summary page
else:
    question = Question.objects.get(id=next_question_id)
    context = {'question': question}
    return render(request, 'quiz_app_question.html', context)

    # Retrieve the question based on the question_id
    question = Question.objects.get(id=question_id)
    context = {'question': question}
    return render(request, 'quiz_app_question.html', context)


def quiz_summary(request):
    '''View to display all entries of answers table'''
    answers = Answer.objects.all()
    context = {'answers': answers}
    return render(request, 'quiz_app_summary.html', context)

10.11.3 Then the urls:

from django.urls import path
from . import views

urlpatterns = [
    path("", views.quiz_app_home, name="quiz_app_home"),
    path('start_quiz/', views.quiz_start, name='quiz_start'),
    path("quiz/questions/", views.quiz_questions, name="quiz_questions"),
    path('quiz/<int:question_id>/', views.quiz_question, name='quiz_question'),
    path('quiz/summary/', views.quiz_summary, name='quiz_summary'),
    path('add-question/', views.quiz_add_question, name='quiz_add_question'),
]

10.11.4 And forms:

from django import forms
from .models import Question

class QuestionForm(forms.ModelForm):
    class Meta:
model = Question
fields = ['description']

10.12 And templates:

10.12.1 quiz_app_add_question.html

{% extends 'base.html' %}

{% block content %}
    <h2>Add Question</h2>
    <form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Add</button>
    </form>
{% endblock %}

10.12.2 quiz_app_home.html

{% extends 'base.html' %}

{% block content %}
    <h1>Welcome to My QUIZ APP</h1>
    <p>Here you can answer some questions bla.</p>
    <p>Quiz is here - <a href="{% url 'quiz_start' %}">Here</a></p>
    <p>all questions can see - <a href="{% url 'quiz_questions' %}">Here</a></p>
{% endblock %}

10.12.3 quiz_app_question.html

{% extends 'base.html' %}

{% block content %}
    <h2>Question:</h2>
    <p>{{ question.description }}</p>
    <form action="{% url 'quiz_question' question_id=question.id %}" method="POST">
{% csrf_token %}
<input type="hidden" name="question_id" value="{{ question.id }}">
<input type="text" name="answer" placeholder="Enter your answer" required>
<button type="submit">Submit</button>
    </form>
{% endblock %}

10.12.4 quiz_app_questions.html

{% extends 'base.html' %}

{% block content %}
    <h1>Questions</h1>
    <table class="table">
<thead>
    <tr>
<th>ID</th>
<th>Description</th>
    </tr>
</thead>
<tbody>
    {% for question in questions %}
<tr>
    <td>{{ question.id }}</td>
    <td>{{ question.description }}</td>
</tr>
    {% endfor %}
</tbody>
    </table>

    <a href="{% url 'quiz_add_question' %}" class="btn btn-primary">Add Question</a>
{% endblock %}

10.12.5 quiz_app_ready.html

{% extends 'base.html' %}

{% block content %}
    <h2>Ready to start the quiz?</h2>
    <form action="{% url 'quiz_start' %}" method="POST">
{% csrf_token %}
<button type="submit" name="start_quiz">Start Quiz</button>
    </form>

    <form action="{% url 'quiz_start' %}" method="POST">
{% csrf_token %}
<button type="submit" name="delete_answers">Delete My Answers for Today</button>
    </form>

    {% for message in messages %}
<div class="flash-message">{{ message }}</div>
    {% endfor %}
{% endblock %}

10.12.6 quiz_app_summary.html

{% extends 'base.html' %}

{% block content %}
    <h2>Quiz Summary</h2>
    <table>
<thead>
    <tr>
<th>Question</th>
<th>Answer</th>
    </tr>
</thead>
<tbody>
    {% for answer in answers %}
    <tr>
<td>{{ answer.question.description }}</td>
<td>{{ answer.answer }}</td>
    </tr>
    {% endfor %}
</tbody>
    </table>
{% endblock %}

10.12.7 Quiz looks like this:

If there are answers to the quiz already for today - you won’t be able to do the quiz. You can delete the entries before that. Weather api implementation

11 code

Using this as an api - Meteo.lt API

Display data from an api in html.

11.1 Step 1 Create a separate django app

add it to the project. Inside the app’s views.py write such code:

import requests
from django.shortcuts import render
from datetime import datetime, timedelta

# Create your views here.

def weather_view(request):
    # Get yesterday's date
    yesterday = datetime.now() - timedelta(days=5)
    date_str = yesterday.strftime("%Y-%m-%d")

    # Make the API request
    response = requests.get(
        f"https://api.meteo.lt/v1/stations/vilniaus-ams/observations/{date_str}"
    )
    data = response.json()

    print(data)

    # Find the desired observation
    desired_observation = None
    for x in data["observations"]:
        if x["observationTimeUtc"] == f"{date_str} 12:00:00":
            desired_observation = x
            break

    # assign the desired_observation to context
    context = {
        "observation": desired_observation,
    }

    # pass that context to the template and render it
    return render(request, "weather/weather_template.html", context)

Then /templates/weather/weather_template.html can be like such:

<!DOCTYPE html>
<html>
<head>
    <title>Weather Information</title>
</head>
<body>
    <h1>Weather Information</h1>
    {% if observation %}
        <p>Observation Time: {{ observation.observationTimeUtc }}</p>
        <p>Air Temperature: {{ observation.airTemperature }}</p>
        <p>Feels Like Temperature: {{ observation.feelsLikeTemperature }}</p>
        <p>Wind Speed: {{ observation.windSpeed }}</p>
        <p>Wind Gust: {{ observation.windGust }}</p>
        <p>Wind Direction: {{ observation.windDirection }}</p>
        <p>Cloud Cover: {{ observation.cloudCover }}</p>
        <p>Sea Level Pressure: {{ observation.seaLevelPressure }}</p>
        <p>Relative Humidity: {{ observation.relativeHumidity }}</p>
        <p>Precipitation: {{ observation.precipitation }}</p>
        <p>Condition Code: {{ observation.conditionCode }}</p>
    {% else %}
        <p>No observation found for the specified time.</p>
    {% endif %}
</body>
</html>

11.2 Step 2 Storing entries from an api in db

Let’s create a DB first. Inside app/model.py file:

from django.db import models

# Create your models here.

class Weather(models.Model):
    date = models.DateField()
    temperature = models.DecimalField(max_digits=5, decimal_places=2)

then in terminal:

python manage.py makemigrations
python manage.py migrate
python manage.py runserver

Now update the view function to include:

from . models import Weather
weather = Weather(date=date_str, temperature=desired_observation.get("airTemperature", ""))
weather.save()

Now whenever you visit the http://127.0.0.1:8000/weather/ page, the data will be fetched and stored in the DB.

11.3 Step 3 Displaying from an api AND storing to db

Its possible to do this in one single views.py file. We must have models and urls set up like so:

models

from django.db import models

# Create your models here.

class Weather(models.Model):
    date = models.DateField()
    temperature = models.DecimalField(max_digits=5, decimal_places=2)

urls

from django.urls import path
from . import views

urlpatterns = [
    path("", views.weather_view, name="weather_view"),
]

views

import requests
from django.shortcuts import render
from datetime import datetime, timedelta
from .models import Weather


def weather_view(request):
    # Get yesterday's date
    yesterday = datetime.now() - timedelta(days=1)
    date_str = yesterday.strftime("%Y-%m-%d")

    # Make the API request to fetch weather data
    api_url = f"https://api.meteo.lt/v1/stations/vilniaus-ams/observations/{date_str}"
    response = requests.get(api_url)
    data_fetched_from_api = response.json()

    # Retrieve desired observation from the fetched data
    desired_observation = None
    for observation in data_fetched_from_api["observations"]:
        if observation["observationTimeUtc"] == f"{date_str} 12:00:00":
            desired_observation = observation
            break

    # Save the weather data to the database
    save_weather_to_db(date_str, desired_observation)

    # Retrieve the latest weather data from the database
    weather_from_db = retrieve_latest_weather()

    # Prepare the context to pass to the template
    context = {
        "observation": desired_observation,
        "weather_from_db": weather_from_db,
    }

    # Render the template with the context
    return render(request, "weather/weather_template.html", context)


def save_weather_to_db(date_str, desired_observation):
    # Save the weather data to the database
    weather = Weather(
        date=date_str,
        temperature=desired_observation.get("airTemperature", "")
    )
    weather.save()


def retrieve_latest_weather():
    # Retrieve the latest weather data from the database
    weather_from_db = Weather.objects.last()
    return weather_from_db

html template

<!DOCTYPE html>
<html>
<head>
    <title>Weather Information</title>
</head>
<body>
    <!-- data that is fetched from an api  -->
    <h1>Weather Information</h1>
    {% if observation %}
        <p>Observation Time: {{ observation.observationTimeUtc }}</p>
        <p>Air Temperature: {{ observation.airTemperature }}</p>
        <p>Feels Like Temperature: {{ observation.feelsLikeTemperature }}</p>
        <p>Wind Speed: {{ observation.windSpeed }}</p>
        <p>Wind Gust: {{ observation.windGust }}</p>
        <p>Wind Direction: {{ observation.windDirection }}</p>
        <p>Cloud Cover: {{ observation.cloudCover }}</p>
        <p>Sea Level Pressure: {{ observation.seaLevelPressure }}</p>
        <p>Relative Humidity: {{ observation.relativeHumidity }}</p>
        <p>Precipitation: {{ observation.precipitation }}</p>
        <p>Condition Code: {{ observation.conditionCode }}</p>
    {% else %}
        <p>No observation found for the specified time.</p>
    {% endif %}

    <!-- data fetched from the db an api  -->
    {% if weather_from_db %}
        <p>Observation Time: {{ weather_from_db.date|date:"Y-m-d" }}</p>
        <p>Temperature: {{ weather_from_db.temperature }}</p>
    {% else %}
        <p>No observation found.</p>
    {% endif %}
</body>
</html>

Now upon each refresh we will get data displayed in html template from an api and also from the db.

DB will populate like so:

Using this DB viewer - SQLite Viewer Web App

And will be displayed like so:

Now have to separate those processes and make it so that ONLY the data from the DB is displayed and the fetch from api and store to db action happens not on each page refresh but at specific times, without my intervention.

11.4 Step 4 Automating fetching and storing into DB with cron

So since we don’t want fetch and storing to DB happen each time we open /website tab, lets clean up the views.py to contain only the information needed for information display.

from django.shortcuts import render
from .models import Weather


def weather_view(request):
    # Retrieve the latest weather data from the database
    weather_from_db = retrieve_latest_weather()

    # Prepare the context to pass to the template
    context = {
        "weather_from_db": weather_from_db,
    }

    # Render the template with the context
    return render(request, "weather/weather_template.html", context)

def retrieve_latest_weather():
    # Retrieve the latest weather data from the database
    weather_from_db = Weather.objects.last()
    return weather_from_db

Let’s add the content from from views.py file to another file called job_weather.py that should be located in the same directory as manage.py

import requests
from datetime import datetime, timedelta
from weather.models import Weather

def save_weather_to_db(date_str, desired_observation):
    # Save the weather data to the database
    weather = Weather(
        date=date_str,
        temperature=desired_observation.get("airTemperature", "")
    )
    weather.save()
    print(f"{date_str} and {desired_observation.get('airTemperature', '')} are saved to the database")

# Get yesterday's date
yesterday = datetime.now() - timedelta(days=1)
date_str = yesterday.strftime("%Y-%m-%d")

# Make the API request to fetch weather data
api_url = f"https://api.meteo.lt/v1/stations/vilniaus-ams/observations/{date_str}"
response = requests.get(api_url)
data_fetched_from_api = response.json()

# Retrieve desired observation from the fetched data
desired_observation = None
for observation in data_fetched_from_api["observations"]:
    if observation["observationTimeUtc"] == f"{date_str} 12:00:00":
        desired_observation = observation
        break

# Save the weather data to the database
save_weather_to_db(date_str, desired_observation)

If we try to run this file now, we get an error saying:

ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.

This means that this newly created file can not make calls and operate PUT operations to our DJANGO database. We need to configure Django settings for script execution outside of a Django project. This enables the proper functioning of Django-related features, including database access via models. Okay, so the solution is to add this a the top of the file:

import os
import django
# Set the DJANGO_SETTINGS_MODULE environment variable
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lifeapi.settings")
# Configure Django settings
django.setup()

Now when we run this python file, the outcome should be:

(venv) arvy@DESKTOP-AUDMJ7D:~/src/lifeapi/lifeapi$ python job_weather.py
2023-06-20 and 25.9 are saved to the database

Check the DB to confirm that additional line was added.

11.5 Step 5 creating cronjob

We don’t want to run this command manually daily, so let’s create a cron job to do this for us once every one minute(make once every 24hours later).

# open cron editor
crontab -e
# add this line at the end of it:
*/1 * * * * /home/arvy/src/lifeapi/venv/bin/python /home/arvy/src/lifeapi/lifeapi/job_weather.py
# close the file
# make it executable
chmod +x job_weather.py

Now our cron job is created, it will use python with all the dependencies from the environment that we have provided and run the job_weather.py file for us each minute.

Great!!