momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

O chestiune de organizare a codului

Python | Sphinx | orar şcolar
2014 nov

În [1] am prezentat un număr de scripturi Python (module Python) care - executate într-o anumită succesiune - creează fişierele ReST necesare obţinerii prin intermediul generatorului Sphinx, a unui site HTML pentru un orar şcolar furnizat într-un tabel HTML.

Primul script constituia un dicţionar care asocia fiecare profesor cu secvenţa orelor acestuia pe orarul care s-a dat şi salva acest dicţionar prin intermediul pachetului standard json. Mai departe, al doilea modul reconstituia dicţionarul respectiv plecând de la fişierul JSON tocmai creat de primul modul şi îl folosea pentru a formula un prim fişier ReST, conţinând "orarul profesorilor"; apoi, al treilea modul proceda similar pentru o formula ReST "orarul claselor"; ş.a.m.d. (orarele disciplinelor, ale zilelor).

A trebui să lansezi pe rând cinci sau şase scripturi (şi aceasta, după oricare modificare "externă" a orarului şcolii) este incomod. Prin urmare avem de organizat şi poate de reformulat codul Python respectiv, pentru a simplifica lucrurile (de exemplu, să fie de lansat un singur script în loc de cinci).

Poate fi tentantă complicarea lucrurilor, concepând întâi nişte modele de bază "profesor", "disciplină", "clasă", etc.; dar nu prea este cazul, fiindcă noi preluăm doar formularea finală a orarului (nu avem de "introdus" şi de gândit datele pe baza cărora s-a generat orarul).

Cel mai simplu este să concatenăm într-un singur fişier scripturile respective; iar astfel, fişierele JSON (folosite iniţial pentru a transfera structuri de date de la un script la altul) devin inutile.

Creem întâi _ORAR/LAR, iar pe subdirectorul LAR aplicăm utilitarul sphinx-quickstart obţinând:

vb@Home:~$ tree -F --dirsfirst _ORAR
_ORAR/
└── LAR/
    ├── _build/
    ├── _static/
    ├── _templates/
    ├── conf.py
    ├── index.rst
    └── Makefile

Scriptul nostru va trebui să constituie şi să adauge în LAR/ fişierele ReST pentru orarul profesorilor, orarul claselor, etc.; apoi, comanda make html (vizată în fişierul Makefile) va prelucra fişierele ReST existente, constituind în directorul _build (pe baza configurărilor din conf.py) site-ul HTML aferent.
Ceea ce va trebui copiat drept "site" de orar pentru o şcoală sau alta este subdirectorul _build/html - nu avem nevoie să creem câte o structură de fişiere pentru fiecare şcoală ("_ORAR/LAR" pentru una, "_ORAR/LGA" pentru alta, etc.) şi va fi suficient să folosim denumiri invariabile pentru fişierele de creat (ca "LAR/orar.rst", "LAR/clase.rst", etc.).

Extindem structura de directoare instituită mai sus, astfel:

vb@Home:~$ tree -F -L 2 --dirsfirst --filelimit 4 _ORAR/
_ORAR/
├── LAR/ [6 entries exceeds filelimit, not opening dir]
├── orar_rst/
│   ├── __init__.py
│   ├── init.py
│   └── _orar.py
├── make_rst.py
└── orar_prof.csv

Am instituit un pachet Python, "orar_rst"; fişierul special __init__.py permite "importarea" conţinutului pachetului, într-un script sau altul. Modulul "_orar.py" din acest pachet va conţine codul concatenat al celor cinci scripturi amintite mai sus şi definind "make_rst.py" astfel:

# make_rst.py
from orar_rst import _orar

vom putea obţine fişierele ReST aşa cum ne-am dorit, printr-o singură comandă:

vb@Home:~/_ORAR$ python make_rst.py

În [1] aveam un caz fericit: orarul şcolii era furnizat printr-un fişier HTML, datele putând fi extrase în mod automat. Dar cazul obişnuit este acela în care orarul şcolii este prezentat în format Adobe Flash, sau JPG - încât datele trebuie "extrase" manual: lansez Gnumeric şi tastez ceea ce văd alături, pe imaginea în care este redat orarul (dar bineînţeles că dacă văd "9 E", eu tastez "9E"). În final, folosesc opţiunea de meniu "Export as CSV file" - obţinând fişierul _ORAR/orar_prof.csv (bineînţeles că păstrez şi o copie separată a acestui fişier, încât atunci când apar modificări în orarul şcolii să nu trebuiască să tastez totul de la început, ci doar să modific fişierul CSV aferent acelei şcoli).

Cum se vede mai sus, încercăm să asigurăm independenţa codului faţă de şcoala al cărei orar îl avem eventual de reformulat ca site HTML; dar pe orarele şcolilor avem o mare varietate de prescurtări şi convenţii de notaţie, pentru numele profesorilor, ale disciplinelor, etc. - şi până la urmă avem de completat câte un fişier "init.py" pentru fiecare şcoală, urmând să-l transferăm în orar_rst/ (ceea ce compromite într-o anumită măsură, "independenţa" codului).

init.py introduce câteva dicţionare Python care explicitează diversele prescurtări şi eventual, specifică unele particularităţi ale încadrării profesorilor:

# encoding=utf-8
acronim = {
    'OUE': u"Organizarea unităţii economice",
    # ... #
}
discip = {
    "Eng": u"Engleză",
    # ... #
}
sarcina = {
    u"Adam Loredana": ['Eng'],
    u"Bercescu Mălina": [{'OUE': ['9D', '9F', '9G']}, 
                         {'PCM': ['10B', '10C', '10D']}],
    # ... #
}

Pe orarele claselor vom folosi cheile "scurte" din dicţionarul acronim (va apărea ora de "OUE", nu "Organizarea unităţii economice" care este prea lung ca să "meargă" pe orarul clasei); dar pe orarul disciplinei, va apărea şi denumirea completă a acesteia. În dicţionarul sarcina folosim deasemenea cheile (denumiri scurte) din dicţionarele acronim şi discip, specificând disciplinele fiecărui profesor.

În legătură cu aceste dicţionare vom avea nevoie de câteva funcţii utilitare şi le adăugăm chiar în modulul init.py. Astfel, pe orarul dat (deci în orar_prof.csv) profesorii sunt indicaţi "scurt", iar în dicţionarul sarcina putem avea numele complet - de unde necesitatea unei asemenea funcţii:

class NotNume(Exception):
    pass

def nume_prof(abbr):
    for key in sarcina.keys():
        if abbr.strip().replace('\"', '') in key:
            return key
    raise NotNume(abbr)

Dacă prescurtarea indicată este prefix al uneia dintre cheile dicţionarului, se returnează această cheie (numele complet); altfel se va semnala eroarea "NotNume". Putem testa direct această funcţie, lansând interpretorul Python şi importând-o din modulul init:

vb@Home:~/_ORAR$ python
>>> from orar_rst.init import nume_prof
>>> print nume_prof("Adam L")
Adam Loredana
>>> 
>>> print nume_prof("Adam G")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "orar_rst/init.py", line 255, in nume_prof
    raise NotNume(abbr)
orar_rst.init.NotNume: Adam G

În modulul _orar constituim dicţionarele orar şi klas (plecând de la fişierul orar_prof.csv) şi pe baza acestora, constituim apoi fişierele ReST dorite:

# encoding=utf-8
import codecs, re, zipfile
from init import acronim, sarcina, discip, dirig, \
                 nume_prof, disci_pq, disci_p, max_len

pad = codecs.open('orar_prof.csv', 'r', 'utf-8')

orar = {} # orar[prof] = [ [ore_zi0], ..., [ore_zi4] ]
for line in pad:
    cols = line.split(',')
    ore60 = [td.strip() or '' for td in cols[1:]]
    orar[nume_prof(cols[0])] = [ore60[z*12:(z+1)*12] for z in range(0, 5)]

klas = {}  # clasă: [(zi, rang_oră, disciplină), ...]
for prof in orar:
    sapt = orar[prof]
    for zi in range(5):
        for ix, ql in enumerate(sapt[zi]):
            if ql:
                if ql not in klas:
                    klas[ql] = []
                obiect = disci_pq(prof, ql)
                if len(obiect) > 15:
                    obiect = obiect[:15]    
                klas[ql].append((zi, ix, obiect))

for ql in klas:  # sublista orelor din prima zi, din a doua, etc.
    ore = klas[ql]
    or_ql = []
    for zi in range(5):
        or_z = [(t[1], t[2]) for t in ore if t[0]==zi]
        or_ql.append(sorted(or_z, key=lambda x: int(x[0])))
    klas[ql] = or_ql

# constituie fişierul 'LAR/orar.rst' ("orarul profesorilor")
out = codecs.open('LAR/orar.rst', 'w', 'utf-8')
out.write(u'Orarul profesorilor\n' + '-'*20 + '\n')
# ... v. [1] ... #
out.close()

# constituie fişierul 'LAR/clase.rst' ("orarul claselor")
# ...  v. [1] ... #

# constituie fişierul 'LAR/obiecte.rst' (orarele disciplinelor)
# ...  v. [1] ... #

# constituie fişierele 'LAR/zi_*.rst' (orarele zilelor)
# ...  v. [1] ... #

# constituie o arhivă conţinând orarele zilelor în format CSV
zf = zipfile.ZipFile('LAR/orare_zi.zip', mode='w')
for zi in range(5):
    csv = ''
    for prof in sorted(orar):
        nume = prof.title()  
        or_zi = orar[prof][zi]
        if are_ore(or_zi):
            csv += nume + ',' + ','.join([ora or '-' for ora in or_zi[:6]]) + \
                          ',' + ','.join([ora or '~' for ora in or_zi[6:]]) + '\n'
    zf.writestr(zile[zi] + '.csv', csv.encode('utf-8'))
zf.close()

În final am constituit - în plus faţă de [1] - o arhivă ZIP conţinând fişierele în format CSV aferente orarelor zilnice. În cursul operaţiei de constituire a site-ului HTML, Sphinx va copia această arhivă în subdirectorul LAR/_static şi adăugând în fişierul index.rst directiva:

:download:`orarele zilelor <orare_zi.zip>` (download)

se asigură utilizatorului posibilitatea de a o descărca (vezi orar_site).

vezi Cărţile mele (de programare)

docerpro | Prev | Next