Python Notes: Picture-of-the-Day

  1. The information presented here is intended for educational use.
  2. The information presented here is provided free of charge, as-is, with no warranty of any kind.
  3. Edit: 2023-12-31

Overview

file-1: HTML test page

<!DOCTYPE html>
<html lang="en">
<head>
    <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta content="#ffffff" name="theme-color">
    <title>Test Pix</title>
    <link href="css/nsr-20170909.css" rel="stylesheet" type="text/css">
    <script src="js/picture-viewer72.js"></script>
</head>
<body lang="en-ca" >
    <h4>Test-Pix</h4>
    <table class="tbl-blue" style="width:100%">
	<tr><td>msg</td> <td id="msg"></td></tr>
	<tr><td>src</td> <td id="src"></td></tr>
	<tr><td style="width:50px">com</td>
<td id="comment" style="min-width:90%"></td>
</tr> <tr><td>txt</td>
<td id="text" style="text-align: center; font-weight: bold;height:280px"></td>
</tr> <tr><td>calc</td><td id="calc"></td></tr> <tr><td>mmdd</td><td id="mmdd"></td></tr> </table> <script> testYear(); </script> </body> </html>

file-2: HTML production page

<!DOCTYPE html>
<html lang="en-ca">
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hackerspace - STEMspace</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<link href="css/nsr-20170909.css" rel="stylesheet" type="text/css"> <script src="js/picture-viewer74.js"></script> </head>
<body>

...snip...

<div class="col-xs-12 col-sm-5 col-md-4 col-lg-3" id="pixoftheday"
style="text-align: center; font-weight: bold;height:280px">
<img alt="400ky of atmospheric CO2" src="images/carbon_dioxide_400kyr.png" style="height:80%"> <div style="text-align:center;margin-top:5px;font-weight:bold">
CO2 levels cycle between<br>180 and 280 ppm every 120K years</div> </div>
</div>

...snip...
<script> //
// uses the namespace technique for declaring a function
// p.s. my implementation is way too complicated for most sites
//
var nsNSR1 = function(){ // // pod (picture-of-day) notes: // 1) In 2022 I want to rotate between 4 images (3 environmental and 1 scientist) // 2) this routine references functions and variables in namespace "nsNSR7" // 3) image "co2-400kyr" is already on the screen in case of failures here // 4) steps: // 1: noop (startup delay to let the DOM settle) // 2: load NOAA co2-data (into slot 1) // 3: load local co2-400kyr (into slot 0) // 4: load NOAA co2-trend (into slot 2) // 5: load local Scientist (into slot 3) // 6: load local co2-data (into slot 1 if it is empty) // 7: load local co2-trend (into slot 2 if it is empty) // 8: stop timer9 then check if we have four images; // if all is well then start timer8 to rotate between four prefetched images // 5) don't go too fast (but it is okay to stack requests) // 1) might need a second to fetch and image url from my database // 2) might need another second to use the url to preload the image (then NOAA is sometimes slow) // function pod(){ // pod = picture-of-day var pix9 = 0; // init console.log("starting pod()"); // start timer-9 var intervalId9 = setInterval( function(){ pix9++; console.log("pod-pix9: "+pix9); if (pix9==1){ // noop console.log("pod-noop"); return; } if (pix9==2){ nsNSR7.fetchDefault(1,8002); // co2-data (NOAS) return; } if (pix9==3){ nsNSR7.fetchDefault(0,8000); // co2 400kyr (local) return; } if (pix9==4){ nsNSR7.fetchDefault(2,8004); // co2-trend (NOAS) return; } if (pix9==5){ nsNSR7.fetchToday(3); // scientist picture-of-the-day return; } if (pix9==6){ // image1: if no 8002 then load 8001 var tmp; try{ tmp=nsNSR7.imageArray[1].width; }catch(e){ tmp=0; } if (tmp==0){ console.log("pod: 8002 fubar so loading 8001"); nsNSR7.fetchDefault(1,8001); // co2-data (local) return; }else{ pix9+=1; console.log("pod-pix9 jumps to: "+pix9); } } if (pix9==7){ // image2: if no 8004 then load 8003 var tmp; try{ tmp=nsNSR7.imageArray[2].width; }catch(e){ tmp=0; } if (tmp==0){ console.log("pod: 8004 fubar so loading 8003"); nsNSR7.fetchDefault(2,8003); // co2-trend-local return; }else{ pix9+=1; console.log("pod-pix9 jumps to: "+pix9); } } if (pix9==8){ console.log("pod: stopping timer #9"); clearInterval(intervalId9); // stop this timer var recipe = 0; // init try{ // make sure we have four good images for (var i=0;i<4;i++){ console.log("image: "+i+" size:",nsNSR7.imageArray[i].width); if (nsNSR7.imageArray[i].width==0){ console.log("oops: image "+i+" is zero length"); return; }else{ recipe++; } } }catch(e){ console.log("recipe error: ",e); recipe = 99; } console.log("recipe: "+recipe); if (recipe != 4){ console.log("pod: recipe error so will not rotate any images"); }else{ console.log("pod: arming timer #8"); var pix8=0; // init var intervalId8 = setInterval( function (){ pix8++; // pix = pix8%4; // only 4 pictures: 0-3 console.log("pod-pix8: " +pix8+" index: "+pix); if (pix8 >= 24){ console.log("pod: stopping timer #8"); clearInterval(intervalId8); }else{ console.log("pod-inject:" + nsNSR7.myTxt[pix]); document.getElementById("pixoftheday").innerHTML = nsNSR7.myTxt[pix]; } }, 5000); // 5 seconds } } }, 600); // 600ms sec } // end of pod() // // expose anything we want externally referenced // return{ pod:pod } }(); // end of nsNSR1 // nsNSR1.pod(); // execute my function </script>
</body> </html>

file-3: JavaScript

// =================================================================
// title : picture-viewer72.js
// author: Neil Rieck (Waterloo, Ontario, Canada)
// edit  : 2020-10-29
// edit  : 2021-10-30 (coincidence?)
// notes :
// v4  (2019-11-28) works with my new server-side Python code
// v5  (2020-04-04) code cleanup
// v6  (2020-10-29) moved everything into a namespace
// v71 (2021-10-30) better overlapping AJAX (pulling from slow sites)
// v72 (2021-10-31) XmlDoc is no longer global (what was I thinking?)
// ==================================================================
//
//	start of name-space
//
var nsNSR6 = (function() {
//
'use strict';		// no kid-stuff here
//
//	some private variables
//
var debug=0;		//
var slot=0;		//
var mySrc=[];		// image source
var myTxt=[];		// image text (from payload)
var imageArray=[];	// image data
//
//	store data as well as preload the image
//
function preloadImage(url,txt,slot){
    if (typeof slot == "undefined"){
	var slot=9;
    }
    console.log("preload url: "+url+" slot: "+slot);
    try{
	mySrc[slot] = url;
	myTxt[slot] = txt;
	imageArray[slot] = new Image();
	imageArray[slot].src = url;
    }catch(e){ 
	console.error("preload url: "+url+" slot: "+slot+" error: ",e);
    }
}
//
//	start_ajax (start an AJAX transaction)
//
function start_ajax(msg,slot){
    console.log("start ajax slot: "+slot);
// still some Microsoft browsers out there if (typeof XMLHttpRequest == "undefined") XMLHttpRequest=function(){ try { return new ActiveXObject("Msxml2.XMLHTTP.6.0") } catch (e) {} try { return new ActiveXObject("Msxml2.XMLHTTP.3.0") } catch (e) {} try { return new ActiveXObject("Msxml2.XMLHTTP") } catch (e) {} try { return new ActiveXObject("Microsoft.XMLHTTP") } catch (e) {} throw new Error("This browser does not support XMLHttpRequest or XMLHTTP.") } if (msg != ""){ var xhr=new XMLHttpRequest(); if (xhr != null){ xhr.open("GET", msg, true); // async=true xhr.timeout=3000; // 3 seconds max xhr.ontimeout = function(){ console.log("Timeout:",slot); } xhr.onreadystatechange=function(){ if (xhr.readyState == 4 && xhr.status == 200){ var resp$=xhr.responseText; ajax_xml_extract(resp$); } } console.log("sending ajax request for slot: "+slot) xhr.send(null); // not null for POST } } } // // ajax_xml_extract // function ajax_xml_extract(resp$){ // console.log("ajax-xml-extract raw:",resp$); try{ if (window.DOMParser) { var parser=new DOMParser(); var xml=parser.parseFromString(resp$,"text/xml"); }else{ var xml=new ActiveXObject("Microsoft.XMLDOM"); xml.async=false; xml.loadXML(resp$); } var src = get_xml_data("src",xml); var txt = get_xml_data("text",xml); var slot= get_xml_data("slot",xml); console.log("calling preloadImage for slot: "+slot) preloadImage(src,txt,slot); }catch(e){ console.error("ajax_xml_extract:",e) } } // // does string 'x' represent a positive integer? // function isStrInt(x){ x=x.replace(/\s/g,''); if (x=="") return false; try{ y = parseInt(x); }catch(e){ y = -1; } if(y>0){ return true; }else{ return false; } } function htmlEncode(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function htmlDecode(str) { return String(str).replace(/</g, '<').replace(/>>/g, '>').replace(/"/g, '"').replace(/&/g, '&'); } // // get_xml_data (look for one item) // function get_xml_data(tag,xml){ var x; try{ x = xml.getElementsByTagName(tag)[0].childNodes[0].nodeValue; }catch(e){ console.error("get-xml-data: ",e); x = null; } return(x); } // // get number of days per month // function monthSize(x){ switch(x) { case 2: return(28); case 9: case 4: case 6: case 11: return(30); default: return(31); } } function pad(num, size) { var s = num+""; while (s.length < size) s = "0" + s; return s; } // // test picture-of-the-day logic for the whole year // caveat: only used in testing // function testYear(){ debug=1; // change global var mm=0; // month var dd=0; // day var mmdd=""; var intervalId = setInterval(function(){ if (mm==0){ mm=1; dd=1; }else{ dd++; limit=monthSize(mm); if (dd>limit){ mm++; dd=1; } } if (mm>12){ clearInterval(intervalId); }else{ mmdd=junk=pad(mm,2)+pad(dd,2); console.log("mmdd:"+mmdd);
document.getElementById("mmdd").innerHTML="mmdd: "+mmdd; msg = "https://neilrieck.net/cgi-bin/daily_pix?xml=1&slot=0&mmdd="+mmdd; start_ajax(msg,0); } }, 1000); } // // get one picture for today // function fetchToday(slot){ var d = new Date(); var mm = d.getMonth(); // 0..11 var dd = d.getDate(); // 1..31 var mmdd= pad(mm+1,2)+pad(dd,2); console.log("fetchToday-mmdd: "+mmdd); var msg = "https://neilrieck.net/cgi-bin/daily_pix?xml=1&slot="+slot+"&mmdd="+mmdd; start_ajax(msg,slot); } // // get default picture // 8000: co2_400kyr // 8001: co2_levels (static-local) // 8002: co2_levels (dynamic-remote) // function fetchDefault(slot,mmdd){ if (typeof mmdd == "undefined"){ var slot=8000; } if ((mmdd<8000)||(mmdd>8002)){ slot=8000; } console.log("fetchDefault-mmdd: "+mmdd+" slot: "+slot); var msg = "https://neilrieck.net/cgi-bin/daily_pix?xml=1&slot="+slot+"&mmdd="+mmdd; start_ajax(msg,slot); } // // end-of-name-space logic // now publish the names of anything referenced externally // return{ preloadImage:preloadImage, fetchToday:fetchToday, fetchDefault:fetchDefault, mySrc:mySrc, myTxt:myTxt, imageArray:imageArray } // end of name-space }()); // end of file

file-4: Python3 (optional launcher)

note: this file is usually located in the cgi-bin folder of your web server
#!/usr/bin/python3
# title : daily_pix
# author: Neil Rieck (Waterloo, Ontario, Canada)
# edit  : 2022-09-30
# notes : forces Python3 to do a one-time compile of whatever is imported so
#         'file_100.py' is compiled to '__pycache__/file_100.cpython-36.pyc'
import daily_pix_133
daily_pix_133.main()

file-5: Python3 (program)

note: in this example, this file is found under cgi-bin (but you could move it anywhere else provided Apache can access it)
#!/usr/bin/python3
'''
===================================================================================
title  : daily_pix_133.py
author : Neil Rieck
history:
ver when       what
100 2019-11-23 calls daily_pix_mysql.py
101 2019-12-15 cleanup
102 2020-04-18 added JSON support
130 2020-05-09 moved db into here; changed the connector; cursor now returns a dict
    2020-07-11 tiny fix in anniversary/season calc (see: 'calc' vs 'calc1')
131 2020-07-11 replaced format strings with f strings
    2021-12-01 removed 'http:' from urls (to properly support CORS)
132 2022-08-22 now also encode 'comment' in output_xml
133 2022-09-30 added support for pattern: +ccyy+
purpose:
1) for a given month-day (format: mmdd) return a picture along with associated text
2) optionally, return the data in XML or JSON format
3) these PEP8 rules are disabled: E305,E501 so I can code in a traditional way
4) I used vim8 along with the pymode plugin
===================================================================================
'''
import cgi
import datetime
import json
import sys
import html
import mysql.connector as db
#
#   constants
#
HOST = "neilrieck.net"
#
#   common function to BAIL (will be visible in the browser's console)
#
def errorExit(msg):
    print("content-type: text/plain; charset=utf-8")
    print("")
    print(msg)
    sys.exit()
#
#   logIt (a simple file-based logger)
#   caveat: if SELinux is enabled then write to /var/log/daily_pix_msg_log.txt
#
def logIt(msg):
    try:
        fn = "daily_pix_msg_log.txt"
        f = open(fn, "a")
        f.write(f"msg: {msg}\r\n")
        f.close()
    except Exception:
        pass
#
#   do that database thing
#
def get_record(parm):
    try:
        con = db.connect(host='127.0.0.1', user='read123', passwd='read123', database='hack1')
        cur = con.cursor(dictionary=True)                           # force return of dict
        cmd = ("select * from pixofday "
               f"where (mmdd1 <= {parm}) and (mmdd2 >= {parm}) "
               "order by concat(mmdd2,mmdd1) limit 1")
        cur.execute(cmd)
        # row = cur.fetchall()                                      # returns a list of tuples (or dicts)
        row = cur.fetchone()                                        # returns a tuple (or dict)
        return row
    except Exception as e:
        msg = f"Exception: {e} while opening database"
        logIt(msg)
        errorExit(msg)
'''
===============================================================

 <<< general description >>>

 1) database field definitions
   0: mmdd1   - start date
   1: mmdd2   - end date
   2: comment -
   3: calc    - used to do an anniversary (or season) calculation
   4: src     - image source
   5: text1   - usually a title associated with the photo
   6: text2
   7: text3
 2) data:
   1: each record will indicate that a certain photo be shown between mmmdd1 and mmdd2
   2: one special record has mmdd1="0000" and mmdd2="9999" so something will always be returned
 3) logic:
   1: the 'src' field contains an image location like these two examples:
      1) local:  /images/Edmond_Halley.jpg
      2) remote: https://www.esrl.noaa.gov/gmd/webdata/ccgg/trends/co2_data_mlo.png
   2: any entry not containing "http" will need a local prefix
   3: text1 to text3 contain HTML so merge them into one string then test for the following patterns:
   4: if we detect the string "+xxx+" then replace it with the resultant 'src' string seen at the end of step 2
   5: if we detect the string "+host+" then replace it with the default HOST of this site
   6: if we detect the string "+calc+" then do an season calculation (eg. 45th season of Quirks+Quarks)
   7: if we detect the string "+calc1+" then do an offset+1 calculation (eg. 50th Anniversary of Oktoberfest)
   8: if we detect the string "+ccyy+" then we will replace it with the current year
   9: return data to caller via XML or JSON (this means the data must be properly escaped)
 4) Cross-Origin Resource Sharing (CORS)
    For AJAX use you must add a few Cross-Origin Resource Sharing (CORS) directives to Apache config like so:
    <Directory "/var/www/html/cgi-bin">
      AllowOverride None
      Options None
      Require all granted
      # 1) CORS for initial AJAX testing.
      #Header set Access-Control-Allow-Origin "*"
      # 2) CORS for single second site support (this Apache instance runs on "https://neilrieck.net"
      # but we want to allow AJAX access from "http://www3.sympatico.ca/n.rieck/")
      Header set Access-Control-Allow-Origin http://www3.sympatico.ca
      Header set Access-Control-Allow-Headers "Content-Type,Accept"
      Header merge Vary Origin
      Header set MyHeader "Neil's Hack"
     </Directory>
===============================================================
'''
#
#   raw data in :: cooked data out
#
def process_data(row):
    imgSrc = row['src']                                             # LOGIC-1
    if (imgSrc.find('http') == -1):                                 # LOGIC-2 (local or remote)
        imgSrc = ".." + imgSrc                                      # would '/images' be better?
    allText = row['text1'] + row['text2'] + row['text3']            # LOGIC-3
    temp = allText.find('+xxx+')                                    # LOGIC-4
    if (temp != -1):                                                # if +xxx+ was found...
        allText = allText[:temp] + imgSrc + allText[temp+5:]        #
    temp = allText.find('+host+')                                   # LOGIC-5
    if (temp != -1):                                                # if +host+ was found...
        allText = allText[:temp] + HOST + allText[temp+6:]          #
    temp = allText.find('+calc+')                                   # LOGIC-6 (season number)
    if (temp != -1):                                                # if +calc+ was found...
        year = datetime.datetime.now().strftime('%Y')               #
        try:                                                        #
            season = int(row['calc'])                               # eg. ccyy
        except Exception as e:                                      #
            season = 1900                                           #
            msg = f"error: {e}"                                     #
            logIt(msg)                                              #
        diff = int(year) - season                                   #
        allText = allText[:temp] + str(diff) + allText[temp+6:]     #
    temp = allText.find('+calc1+')                                  # LOGIC-7 (anniversary)
    if (temp != -1):                                                # if +calc1+ was found...
        year = datetime.datetime.now().strftime('%Y')               #
        try:                                                        #
            anniv = int(row['calc'])                                # eg. ccyy
        except Exception as e:                                      #
            anniv = 1900                                            #
            msg = f"error: {e}"                                     #
            logIt(msg)                                              #
        diff1 = int(year) - anniv + 1                               #
        allText = allText[:temp] + str(diff1) + allText[temp+7:]    #
    temp = allText.find('+ccyy+')                                   # LOGIC-8
    if (temp != -1):                                                # if +ccyy+ was found...
        year = datetime.datetime.now().strftime('%Y')               #
        allText = allText[:temp] + str(year) + allText[temp+6:]     #
    return allText, imgSrc                                          #

#
#   output xml
#
def output_xml(row, mmdd, slot):
    allText, imgSrc = process_data(row)                             #
    escText = html.escape(allText)                                  # LOGIC-9
    comment = row['comment']                                        #
    escComm = html.escape(comment)                                  #
    calc = row['calc']                                              #
    print('Content-Type: text/xml; charset=utf-8')                  # we're returning an XML response
    print('')                                                       # end of HTTP header
    print('<?xml version="1.0" encoding="utf-8"?>')                 # start of XML content
    print('<result>')                                               # user-defined by me (see JavaScript)
    print('<status>1</status>')                                     #
    print(f'<debug>{mmdd}</debug>')                                 # the parameter passed in
    print(f'<comment>{escComm}</comment>')                          #
    print(f'<calc>{calc}</calc>')                                   #
    print(f'<src>{imgSrc}</src>')                                   #
    print(f'<text>{escText}</text>')                                #
    print(f'<slot>{slot}</slot>')                                   #
    print('</result>')                                              #
#
#   output json
#
def output_json(row, mmdd, slot):
    allText, imgSrc = process_data(row)                             # cook the data
    print('Content-Type: text/json; charset=utf-8')                 # we're returning an XML response
    print('')                                                       # end of HTTP header
    del row['text3']                                                # remove raw
    del row['text2']
    del row['text1']
    row['imgSrc'] = imgSrc                                          # append cooked
    row['text'] = allText                                           # append cooked
    print(json.dumps(row, indent=1))                                # magic happens here
#
def output_plain(row, mmdd, slot):                                  #
    allText, imgSrc = process_data(row)                             #
    print('Content-Type: text/plain; charset=utf-8')                #
    print('')                                                       #
    print("debug  : " + mmdd)                                       #
    print("comment: " + row['comment'])                             # output: comment field
    print("calc   : " + row['calc'])                                #
    print("src    : " + imgSrc)                                     # output: IMG src field
    print("text   : " + allText)                                    # output: text fields

# ==============================================================
# main block
#
# example URLs:
# Dec-25) https://neilrieck.net/cgi-bin/daily_pix?mmdd=1225&xml=1
# Jan-01) https://neilrieck.net/cgi-bin/daily_pix?mmdd=0101&xml=1
# CO2   ) https://neilrieck.net/cgi-bin/daily_pix?mmdd=8000&xml=1
# ==============================================================
#
#   locate named data in cgi field storage
#
def getFld(cfs, label, dflt):
    if label not in cfs:
        return dflt
    else:
        return cfs.getvalue(label)

def main():
    cfs = cgi.FieldStorage()                                        # convert CGI data into a Python data
    xml = getFld(cfs, 'xml', '0')                                   # default to "0" if missing
    json = getFld(cfs, 'json', '0')                                 #
    slot = getFld(cfs, 'slot', '0')                                 # slot may be useful when caching an image
    mmdd = getFld(cfs, 'mmdd', '0212')                              # default to Charles Darwin's birthday
    try:
        x = int(mmdd)                                               #
        if (x < 0):                                                 #
            mmdd = "0000"                                           #
        if (x > 9999):                                              #
            mmdd = "9999"                                           #
    except Exception:                                               #
        mmdd = "0212"                                               #
    #
    #   start by logging this event (default privs may not allow this)
    #   caveat: if SELinux is enabled then we may need to write to /var/log
    #
    try:
        snap = datetime.datetime.now()                              # snapshot of the current time
        date = snap.strftime('%Y%m%d')                              #
        fn = f"dp-log/dp_{date}.txt"                                # all logs go into a daily file
        f = open(fn, "a")                                           #
        dt = snap.strftime('%Y%m%d%H%M%S')                          #
        f.write(f"time: {dt}\r\n")                                  #
        f.write(f"mmdd: {mmdd} xml: {xml} json: {json})\r\n")       #
        f.close()                                                   #
    except Exception as e:                                          #
        msg = f"error: {e} in logger"                               #
        logIt(msg)                                                  #
        pass                                                        #
    #
    row = get_record(mmdd)                                          # fetch one record from our database
    #
    # return data in the requested format
    #
    if (json == "1"):
        output_json(row, mmdd, slot)                                # output JSON data
    elif (xml == "1"):
        output_xml(row, mmdd, slot)                                 # output xml data
    else:
        output_plain(row, mmdd, slot)                               # output plain data

#
#   catch-all
#
if __name__ == '__main__':
    main()
#
# === end-of-file

External Links


Back to Home
 Neil Rieck
 Waterloo, Ontario, Canada.