From 81fae86b493bfa7c9283a48e7b8b45febc2b8bc3 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 1 Mar 2020 13:44:21 -0600 Subject: [PATCH] Bring back a devel server for debugging --- CHANGELOG.md | 7 +- cmd/mothd/httpd.go | 7 +- cmd/mothd/mothballs.go | 4 + devel/answer_words.txt | 7713 ++++++++++++++++++++++++++++++++++++++++ devel/devel-server.py | 287 ++ devel/mistune.py | 1190 +++++++ devel/moth.py | 469 +++ devel/mothballer.py | 108 + devel/mothd.service | 18 + devel/parse.py | 1335 +++++++ devel/setup.cfg | 8 + devel/update-words.sh | 19 + devel/validate.py | 229 ++ theme/moth.js | 3 +- 14 files changed, 11393 insertions(+), 4 deletions(-) create mode 100644 devel/answer_words.txt create mode 100755 devel/devel-server.py create mode 100644 devel/mistune.py create mode 100644 devel/moth.py create mode 100755 devel/mothballer.py create mode 100644 devel/mothd.service create mode 100644 devel/parse.py create mode 100644 devel/setup.cfg create mode 100755 devel/update-words.sh create mode 100644 devel/validate.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ea2248e..db48106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [4.0.0] - Unreleased ### Added - New `transpile` command to replace some functionality of devel server ### Changed - Major rewrite/refactor of `mothd` + - There are now providers for State, Puzzles, and Theme. Sqlite, Redis, or S3 should fit in easily. + - Server no longer provides unlocked content + - Puzzle URLs are now just `/content/${cat}/${points}/` - `state/until` is now `state/hours` and can specify multiple begin/end hours - `state/disabled` is now `state/enabled` - Mothball structure has changed substantially + - Mothballs no longer contain `map.txt` + - Clients now expect unlocked puzzles to just be `map[string][]int` ### Deprecated diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index 38e3d76..831905c 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -94,8 +94,11 @@ func (h *HTTPServer) StateHandler(w http.ResponseWriter, req *http.Request) { state.TeamNames = export.TeamNames state.PointsLog = export.PointsLog - // XXX: unstub this - state.Puzzles = map[string][]int{"sequence": {1}} + state.Puzzles = make(map[string][]int) + log.Println("Puzzles") + for category := range h.Puzzles.Inventory() { + log.Println(category) + } JSONWrite(w, state) } diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index 7af6ae3..eb9c3f3 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -30,6 +30,10 @@ func (m *Mothballs) Open(cat string, points int, filename string) (io.ReadCloser } func (m *Mothballs) Inventory() []Category { + for cat, zfs := range m.categories { + map, err := zfs.Open("map.txt") + log.Println("mothballs", cat, zfs) + } return []Category{} } diff --git a/devel/answer_words.txt b/devel/answer_words.txt new file mode 100644 index 0000000..7adc1a8 --- /dev/null +++ b/devel/answer_words.txt @@ -0,0 +1,7713 @@ +about +search +other +information +which +their +there +contact +business +online +first +would +services +these +click +service +price +people +state +email +health +world +products +music +should +product +system +policy +number +please +available +copyright +support +message +after +software +video +where +rights +public +books +school +through +links +review +years +order +privacy +items +company +group +under +general +research +university +january +reviews +program +games +management +could +great +united +hotel +international +center +store +travel +comments +development +report +member +details +terms +before +hotels +right +because +local +those +using +results +office +education +national +design +posted +internet +address +community +within +states +phone +shipping +reserved +subject +between +forum +family +based +black +check +special +prices +website +index +being +women +today +technology +south +project +pages +version +section +found +sports +house +related +security +county +american +photo +members +power +while +network +computer +systems +three +total +place +following +download +without +access +think +north +resources +current +posts +media +control +water +history +pictures +personal +since +including +guide +directory +board +location +change +white +small +rating +government +children +during +return +students +shopping +account +times +sites +level +digital +profile +previous +events +hours +image +department +title +description +insurance +another +shall +property +class +still +money +quality +every +listing +content +country +private +little +visit +tools +reply +customer +december +compare +movies +include +college +value +article +provide +source +author +different +press +learn +around +print +course +canada +process +stock +training +credit +point +science +categories +advanced +sales +english +estate +conditions +select +windows +photos +thread +category +large +gallery +table +register +however +october +november +market +library +really +action +start +series +model +features +industry +human +provided +required +second +accessories +movie +forums +march +september +better +questions +yahoo +going +medical +friend +server +study +application +staff +articles +feedback +again +looking +issues +april +never +users +complete +street +topic +comment +financial +things +working +against +standard +person +below +mobile +party +payment +equipment +login +student +programs +offers +legal +above +recent +stores +problem +memory +performance +social +august +quote +language +story +options +experience +rates +create +young +america +important +field +paper +single +activities +example +girls +additional +password +latest +something +question +changes +night +texas +poker +status +browse +issue +range +building +seller +court +february +always +result +audio +light +write +offer +groups +given +files +event +release +analysis +request +china +making +picture +needs +possible +might +professional +month +major +areas +future +space +committee +cards +problems +london +washington +meeting +become +interest +child +enter +california +share +similar +garden +schools +million +added +reference +companies +listed +learning +energy +delivery +popular +stories +computers +journal +reports +welcome +central +images +president +notice +original +radio +until +color +council +includes +track +australia +discussion +archive +others +entertainment +agreement +format +least +society +months +safety +friends +trade +edition +messages +marketing +further +updated +association +having +provides +david +already +green +studies +close +common +drive +specific +several +living +collection +called +short +display +limited +powered +solutions +means +director +daily +beach +natural +whether +electronics +period +planning +database +official +weather +average +technical +window +france +region +island +record +direct +microsoft +conference +environment +records +district +calendar +costs +style +front +statement +update +parts +downloads +early +miles +sound +resource +present +applications +either +document +works +material +written +federal +hosting +rules +final +adult +tickets +thing +centre +requirements +cheap +finance +minutes +third +gifts +europe +reading +topics +individual +cover +usually +together +videos +percent +function +getting +global +economic +player +projects +lyrics +often +subscribe +submit +germany +amount +watch +included +though +thanks +everything +deals +various +words +linux +production +commercial +james +weight +heart +advertising +received +choose +treatment +newsletter +archives +points +knowledge +magazine +error +camera +currently +construction +registered +clear +receive +domain +methods +chapter +makes +protection +policies +beauty +manager +india +position +taken +listings +models +michael +known +cases +engineering +florida +simple +quick +wireless +license +friday +whole +annual +published +later +basic +shows +corporate +google +church +method +purchase +customers +active +response +practice +hardware +figure +materials +holiday +enough +designed +along +among +death +writing +speed +countries +brand +discount +higher +effects +created +remember +standards +yellow +political +increase +advertise +kingdom +environmental +thought +stuff +french +storage +japan +doing +loans +shoes +entry +nature +orders +availability +africa +summary +growth +notes +agency +monday +european +activity +although +western +income +force +employment +overall +river +commission +package +contents +players +engine +album +regional +supplies +started +administration +institute +views +plans +double +build +screen +exchange +types +sponsored +lines +electronic +continue +across +benefits +needed +season +apply +someone +anything +printer +condition +effective +believe +organization +effect +asked +sunday +selection +casino +volume +cross +anyone +mortgage +silver +corporation +inside +solution +mature +rather +weeks +addition +supply +nothing +certain +executive +running +lower +necessary +union +jewelry +according +clothing +particular +names +robert +homepage +skills +islands +advice +career +military +rental +decision +leave +british +teens +woman +facilities +sellers +middle +cable +opportunities +taking +values +division +coming +tuesday +object +lesbian +appropriate +machine +length +actually +score +statistics +client +returns +capital +follow +sample +investment +shown +saturday +christmas +england +culture +flash +george +choice +starting +registration +thursday +courses +consumer +airport +foreign +artist +outside +furniture +levels +channel +letter +phones +ideas +wednesday +structure +summer +allow +degree +contract +button +releases +homes +super +matter +custom +virginia +almost +located +multiple +asian +distribution +editor +industrial +cause +potential +focus +featured +rooms +female +responsible +communications +associated +thomas +primary +cancer +numbers +reason +browser +spring +foundation +answer +voice +friendly +schedule +documents +communication +purpose +feature +comes +police +everyone +independent +approach +cameras +brown +physical +operating +medicine +ratings +chicago +forms +glass +happy +smith +wanted +developed +thank +unique +survey +prior +telephone +sport +ready +animal +sources +mexico +population +regular +secure +navigation +operations +therefore +simply +evidence +station +christian +round +paypal +favorite +understand +option +master +valley +recently +probably +rentals +built +publications +blood +worldwide +improve +connection +publisher +larger +networks +earth +parents +nokia +impact +transfer +introduction +kitchen +strong +carolina +wedding +properties +hospital +ground +overview +accommodation +owners +disease +excellent +italy +perfect +opportunity +classic +basis +command +cities +william +express +award +distance +peter +assessment +ensure +involved +extra +especially +interface +partners +budget +rated +guides +success +maximum +operation +existing +quite +selected +amazon +patients +restaurants +beautiful +warning +locations +horse +forward +flowers +stars +significant +lists +technologies +owner +retail +animals +useful +directly +manufacturer +providing +housing +takes +bring +catalog +searches +trying +mother +authority +considered +traffic +programme +joined +input +strategy +agent +valid +modern +senior +ireland +teaching +grand +testing +trial +charge +units +instead +canadian +normal +wrote +enterprise +ships +entire +educational +leading +metal +positive +fitness +chinese +opinion +football +abstract +output +funds +greater +likely +develop +employees +artists +alternative +processing +responsibility +resolution +guest +seems +publication +relations +trust +contains +session +multi +photography +republic +components +vacation +century +academic +assistance +completed +graphics +indian +expected +grade +dating +pacific +mountain +organizations +filter +mailing +vehicle +longer +consider +northern +behind +panel +floor +german +buying +match +proposed +default +require +outdoor +morning +otherwise +allows +protein +plant +reported +transportation +politics +partner +disclaimer +authors +boards +faculty +parties +membership +mission +string +sense +modified +released +stage +internal +goods +recommended +unless +richard +detailed +japanese +approved +background +target +except +character +maintenance +ability +maybe +functions +moving +brands +places +pretty +trademarks +phentermine +spain +southern +yourself +winter +battery +youth +pressure +submitted +boston +keywords +medium +television +interested +break +purposes +throughout +dance +itself +defined +papers +playing +awards +studio +reader +virtual +device +established +answers +remote +programming +external +apple +regarding +instructions +offered +theory +enjoy +remove +surface +minimum +visual +variety +teachers +martin +manual +block +subjects +agents +increased +repair +civil +steel +understanding +songs +fixed +wrong +beginning +hands +associates +finally +updates +desktop +classes +paris +sector +capacity +requires +jersey +fully +father +electric +instruments +quotes +officer +driver +businesses +respect +unknown +specified +restaurant +worth +procedures +teacher +relationship +workers +georgia +peace +traditional +campus +showing +creative +coast +benefit +progress +funding +devices +grant +agree +fiction +sometimes +watches +careers +beyond +families +museum +themselves +transport +interesting +blogs +evaluation +accepted +former +implementation +complex +galleries +references +presented +agencies +literature +respective +parent +spanish +michigan +columbia +setting +scale +stand +economy +highest +helpful +monthly +critical +frame +musical +definition +secretary +angeles +networking +australian +employee +chief +gives +bottom +magazines +packages +detail +francisco +changed +heard +begin +individuals +colorado +royal +clean +switch +russian +largest +african +titles +relevant +guidelines +justice +connect +bible +basket +applied +weekly +installation +described +demand +suite +vegas +square +chris +attention +advance +auction +difference +allowed +correct +charles +nation +selling +piece +sheet +seven +older +illinois +regulations +elements +species +cells +module +resort +facility +random +pricing +certificate +minister +motion +looks +fashion +directions +visitors +documentation +monitor +trading +forest +calls +whose +coverage +couple +giving +chance +vision +ending +clients +actions +listen +discuss +accept +automotive +naked +successful +communities +clinical +situation +sciences +markets +lowest +highly +publishing +appear +emergency +developing +lives +currency +leather +determine +temperature +announcements +patient +actual +historical +stone +commerce +ringtones +perhaps +persons +difficult +scientific +satellite +tests +village +accounts +amateur +particularly +factors +coffee +settings +buyer +cultural +steve +easily +poster +functional +closed +holidays +zealand +balance +monitoring +graduate +replies +architecture +initial +label +thinking +scott +recommend +canon +league +waste +minute +provider +optional +dictionary +accounting +manufacturing +sections +chair +fishing +effort +phase +fields +fantasy +letters +motor +professor +context +install +shirt +apparel +generally +continued +crime +count +breast +techniques +johnson +quickly +dollars +websites +religion +claim +driving +permission +surgery +patch +measures +generation +kansas +chemical +doctor +reduce +brought +himself +component +enable +exercise +santa +guarantee +leader +diamond +israel +processes +servers +alone +meetings +seconds +jones +arizona +keyword +interests +flight +congress +username +produced +italian +paperback +classifieds +supported +pocket +saint +freedom +argument +competition +creating +drugs +joint +premium +providers +fresh +characters +attorney +upgrade +factor +growing +thousands +stream +apartments +hearing +eastern +auctions +therapy +entries +dates +generated +signed +upper +administrative +serious +prime +samsung +limit +began +louis +steps +errors +shops +efforts +informed +thoughts +creek +worked +quantity +urban +practices +sorted +reporting +essential +myself +tours +platform +affiliate +labor +immediately +admin +nursing +defense +machines +designated +heavy +covered +recovery +integrated +configuration +merchant +comprehensive +expert +universal +protect +solid +presentation +languages +became +orange +compliance +vehicles +prevent +theme +campaign +marine +improvement +guitar +finding +pennsylvania +examples +saying +spirit +claims +challenge +motorola +acceptance +strategies +affairs +touch +intended +towards +goals +election +suggest +branch +charges +serve +affiliates +reasons +magic +mount +smart +talking +latin +multimedia +avoid +certified +manage +corner +computing +oregon +element +birth +virus +abuse +interactive +requests +separate +quarter +procedure +leadership +tables +define +racing +religious +facts +breakfast +column +plants +faith +chain +developer +identify +avenue +missing +approximately +domestic +sitemap +recommendations +moved +houston +reach +comparison +mental +viewed +moment +extended +sequence +attack +sorry +centers +opening +damage +reserve +recipes +gamma +plastic +produce +placed +truth +counter +failure +follows +weekend +dollar +ontario +automatically +minnesota +films +bridge +native +williams +movement +printing +baseball +owned +approval +draft +chart +played +contacts +jesus +readers +clubs +jackson +equal +adventure +matching +offering +shirts +profit +leaders +posters +institutions +assistant +variable +advertisement +expect +parking +headlines +yesterday +compared +determined +wholesale +workshop +russia +codes +kinds +extension +seattle +statements +golden +completely +teams +lighting +senate +forces +funny +brother +turned +portable +tried +electrical +applicable +returned +pattern +named +theatre +laser +earlier +manufacturers +sponsor +classical +warranty +dedicated +indiana +direction +harry +basketball +objects +delete +evening +assembly +nuclear +taxes +mouse +signal +criminal +issued +brain +sexual +wisconsin +powerful +dream +obtained +false +flower +personnel +passed +supplied +identified +falls +opinions +promote +stated +stats +hawaii +professionals +appears +carry +decided +covers +advantage +hello +designs +maintain +tourism +priority +newsletters +adults +clips +savings +graphic +payments +estimated +binding +brief +ended +winning +eight +anonymous +straight +script +served +wants +miscellaneous +prepared +dining +alert +integration +atlanta +dakota +interview +framework +installed +queen +credits +clearly +handle +sweet +criteria +pubmed +massachusetts +diego +associate +truck +behavior +enlarge +frequently +revenue +measure +changing +votes +looked +discussions +festival +laboratory +ocean +flights +experts +signs +depth +whatever +logged +laptop +vintage +train +exactly +explore +maryland +concept +nearly +eligible +checkout +reality +forgot +handling +origin +gaming +feeds +billion +destination +scotland +faster +intelligence +dallas +bought +nations +route +followed +specifications +broken +tripadvisor +frank +alaska +battle +residential +anime +speak +decisions +industries +protocol +query +partnership +editorial +expression +equity +provisions +speech +principles +suggestions +rural +shared +sounds +replacement +strategic +judge +economics +bytes +forced +compatible +fight +apartment +height +speaker +filed +netherlands +obtain +consulting +recreation +offices +designer +remain +managed +failed +marriage +korea +banks +participants +secret +kelly +leads +negative +austin +favorites +toronto +theater +springs +missouri +andrew +perform +healthy +translation +estimates +assets +injury +joseph +ministry +drivers +lawyer +figures +married +protected +proposal +sharing +philadelphia +portal +waiting +birthday +gratis +banking +officials +brian +toward +slightly +assist +conduct +contained +lingerie +legislation +calling +parameters +serving +profiles +miami +comics +matters +houses +postal +relationships +tennessee +controls +breaking +combined +ultimate +wales +representative +frequency +introduced +minor +finish +departments +residents +noted +displayed +reduced +physics +spent +performed +extreme +samples +davis +daniel +reviewed +forecast +removed +helps +singles +administrator +cycle +amounts +contain +accuracy +sleep +pharmacy +brazil +creation +static +scene +hunter +addresses +crystal +famous +writer +chairman +violence +oklahoma +speakers +drink +academy +dynamic +gender +permanent +agriculture +cleaning +constitutes +portfolio +practical +delivered +collectibles +infrastructure +exclusive +concerns +colour +vendor +originally +intel +utilities +philosophy +regulation +officers +reduction +referred +supports +nutrition +recording +regions +junior +rings +meaning +secondary +wonderful +ladies +henry +ticket +announced +guess +agreed +prevention +soccer +import +posting +presence +instant +mentioned +automatic +healthcare +viewing +maintained +increasing +majority +connected +christ +directors +aspects +austria +ahead +participation +scheme +utility +preview +manner +matrix +containing +combination +devel +amendment +despite +strength +guaranteed +turkey +libraries +proper +distributed +degrees +singapore +enterprises +delta +seeking +inches +phoenix +convention +shares +principal +daughter +standing +comfort +colors +cisco +ordering +alpha +appeal +cruise +bonus +certification +previously +bookmark +buildings +specials +disney +household +batteries +adobe +smoking +becomes +drives +alabama +improved +trees +achieve +positions +dress +subscription +dealer +contemporary +nearby +carried +happen +exposure +panasonic +permalink +signature +gambling +refer +miller +provision +outdoors +clothes +caused +luxury +babes +frames +certainly +indeed +newspaper +circuit +layer +printed +removal +easier +liability +trademark +printers +adding +kentucky +mostly +taylor +trackback +prints +spend +factory +interior +revised +americans +optical +promotion +relative +amazing +clock +identity +suites +conversion +feeling +hidden +reasonable +victoria +serial +relief +revision +broadband +influence +ratio +importance +planet +webmaster +copies +recipe +permit +seeing +proof +tennis +prescription +bedroom +empty +instance +licensed +orlando +specifically +bureau +maine +represent +conservation +ideal +specs +recorded +pieces +finished +parks +dinner +lawyers +sydney +stress +cream +trends +discover +patterns +boxes +louisiana +hills +javascript +fourth +advisor +marketplace +aware +wilson +shape +evolution +irish +certificates +objectives +stations +suggested +remains +greatest +firms +concerned +operator +structures +generic +encyclopedia +usage +charts +continuing +mixed +census +interracial +competitive +exist +wheel +transit +suppliers +compact +poetry +lights +tracking +angel +keeping +preparation +attempt +receiving +matches +accordance +width +noise +engines +forget +array +discussed +accurate +stephen +elizabeth +climate +reservations +playstation +alcohol +greek +instruction +managing +annotation +sister +differences +walking +explain +smaller +newest +establish +happened +expressed +extent +sharp +lesbians +paragraph +mathematics +compensation +export +managers +aircraft +modules +sweden +conflict +conducted +versions +employer +occur +percentage +knows +mississippi +describe +concern +backup +requested +citizens +connecticut +heritage +personals +immediate +holding +trouble +spread +coach +kevin +agricultural +expand +supporting +audience +assigned +jordan +collections +participate +specialist +affect +virgin +experienced +investigation +raised +institution +directed +dealers +searching +sporting +helping +affected +totally +plate +expenses +indicate +blonde +proceedings +favourite +transmission +anderson +characteristics +organic +experiences +albums +cheats +extremely +verzeichnis +contracts +guests +hosted +diseases +concerning +developers +equivalent +chemistry +neighborhood +nevada +thailand +variables +agenda +anyway +continues +tracks +advisory +curriculum +logic +template +prince +circle +grants +anywhere +psychology +responses +atlantic +circumstances +edward +investor +identification +leaving +wildlife +appliances +elementary +cooking +speaking +sponsors +unlimited +respond +sizes +plain +entered +launch +checking +costa +belgium +printable +guidance +trail +enforcement +symbol +crafts +highway +buddy +hardcover +observed +setup +booking +glossary +fiscal +celebrity +styles +denver +filled +channels +ericsson +appendix +notify +blues +chocolate +portion +scope +hampshire +supplier +cables +cotton +bluetooth +controlled +requirement +authorities +biology +dental +killed +border +ancient +debate +representatives +starts +pregnancy +causes +arkansas +biography +leisure +attractions +learned +transactions +notebook +explorer +historic +attached +opened +husband +disabled +authorized +crazy +upcoming +britain +concert +retirement +scores +financing +efficiency +comedy +adopted +efficient +weblog +linear +commitment +specialty +bears +carrier +edited +constant +mouth +jewish +meter +linked +portland +interviews +concepts +reflect +deliver +wonder +lessons +fruit +begins +qualified +reform +alerts +treated +discovery +mysql +classified +relating +assume +confidence +alliance +confirm +neither +lewis +howard +offline +leaves +engineer +lifestyle +consistent +replace +clearance +connections +inventory +converter +organisation +checks +reached +becoming +safari +objective +indicated +sugar +stick +securities +allen +relation +enabled +genre +slide +montana +volunteer +tested +democratic +enhance +switzerland +exact +bound +parameter +adapter +processor +formal +dimensions +contribute +hockey +storm +micro +colleges +laptops +showed +challenges +editors +threads +supreme +brothers +recognition +presents +submission +dolls +estimate +encourage +regulatory +inspection +consumers +cancel +limits +territory +transaction +manchester +weapons +paint +delay +pilot +outlet +contributions +continuous +czech +resulting +cambridge +initiative +novel +execution +disability +increases +ultra +winner +idaho +contractor +episode +examination +potter +plays +bulletin +indicates +modify +oxford +truly +epinions +painting +committed +extensive +affordable +universe +candidate +databases +patent +outstanding +eating +perspective +planned +watching +lodge +messenger +mirror +tournament +consideration +discounts +sterling +sessions +kernel +stocks +buyers +journals +catalogue +jennifer +antonio +charged +broad +taiwan +chosen +greece +swiss +sarah +clark +labour +terminal +publishers +nights +behalf +caribbean +liquid +nebraska +salary +reservation +foods +gourmet +guard +properly +orleans +saving +remaining +empire +resume +twenty +newly +raise +prepare +avatar +depending +illegal +expansion +hundreds +lincoln +helped +premier +tomorrow +purchased +decide +consent +drama +visiting +performing +downtown +keyboard +contest +collected +bands +suitable +absolutely +millions +lunch +audit +chamber +guinea +findings +muscle +featuring +implement +clicking +scheduled +polls +typical +tower +yours +calculator +significantly +chicken +temporary +attend +shower +sending +jason +tonight +sufficient +holdem +shell +province +catholic +awareness +vancouver +governor +seemed +contribution +measurement +swimming +spyware +formula +constitution +packaging +solar +catch +pakistan +reliable +consultation +northwest +doubt +finder +unable +periods +classroom +tasks +democracy +attacks +wallpaper +merchandise +const +resistance +doors +symptoms +resorts +biggest +memorial +visitor +forth +insert +baltimore +gateway +alumni +drawing +candidates +charlotte +ordered +biological +fighting +transition +happens +preferences +romance +instrument +bruce +split +themes +powers +heaven +pregnant +twice +classification +focused +egypt +physician +hollywood +bargain +wikipedia +cellular +norway +vermont +asking +blocks +normally +spiritual +hunting +diabetes +shift +bodies +photographs +cutting +simon +writers +marks +flexible +loved +favourites +mapping +numerous +relatively +birds +satisfaction +represents +indexed +pittsburgh +superior +preferred +saved +paying +cartoon +shots +intellectual +moore +granted +choices +carbon +spending +comfortable +magnetic +interaction +listening +effectively +registry +crisis +outlook +massive +denmark +employed +bright +treat +header +poverty +formed +piano +sheets +patrick +experimental +puerto +revolution +consolidation +displays +plasma +allowing +earnings +mystery +landscape +dependent +mechanical +journey +delaware +bidding +consultants +risks +banner +applicant +charter +barbara +cooperation +counties +acquisition +ports +implemented +directories +recognized +dreams +blogger +notification +licensing +stands +teach +occurred +textbooks +rapid +hairy +diversity +cleveland +reverse +deposit +seminar +investments +latina +wheels +specify +accessibility +dutch +sensitive +templates +formats +depends +boots +holds +router +concrete +editing +poland +folder +womens +completion +upload +pulse +universities +technique +contractors +voting +courts +notices +subscriptions +calculate +detroit +alexander +broadcast +converted +metro +toshiba +anniversary +improvements +strip +specification +pearl +accident +accessible +accessory +resident +possibly +airline +typically +representation +regard +exists +arrangements +smooth +conferences +uniprotkb +strike +consumption +birmingham +flashing +narrow +afternoon +threat +surveys +sitting +putting +consultant +controller +ownership +committees +legislative +researchers +vietnam +trailer +castle +gardens +missed +malaysia +unsubscribe +antique +labels +willing +molecular +acting +heads +stored +logos +residence +attorneys +antiques +density +hundred +operators +strange +sustainable +philippines +statistical +mention +innovation +employers +parallel +honda +amended +operate +bills +bathroom +stable +opera +definitions +doctors +lesson +cinema +asset +elections +drinking +reaction +blank +enhanced +entitled +severe +generate +stainless +newspapers +hospitals +deluxe +humor +monitors +exception +lived +duration +successfully +indonesia +pursuant +fabric +visits +primarily +tight +domains +capabilities +contrast +recommendation +flying +recruitment +berlin +organized +siemens +adoption +improving +expensive +meant +capture +pounds +buffalo +organisations +plane +explained +programmes +desire +expertise +mechanism +camping +jewellery +meets +welfare +caught +eventually +marked +driven +measured +medline +bottle +agreements +considering +innovative +marshall +massage +rubber +conclusion +closing +tampa +thousand +legend +grace +susan +adams +python +monster +villa +columns +disorders +collaboration +hamilton +detection +cookies +inner +formation +tutorial +engineers +entity +cruises +holder +proposals +moderator +tutorials +settlement +portugal +lawrence +roman +duties +valuable +collectables +ethics +forever +dragon +captain +fantastic +imagine +brings +heating +governments +purchasing +scripts +stereo +appointed +taste +dealing +commit +operational +airlines +liberal +livecam +trips +sides +turns +corresponding +descriptions +cache +jacket +determination +animation +oracle +matthew +lease +productions +aviation +hobbies +proud +excess +disaster +console +commands +telecommunications +instructor +giant +achieved +injuries +shipped +seats +approaches +alarm +voltage +anthony +nintendo +usual +loading +stamps +appeared +franklin +angle +vinyl +highlights +mining +designers +melbourne +ongoing +worst +imaging +betting +scientists +liberty +wyoming +blackjack +argentina +convert +possibility +analyst +commissioner +dangerous +garage +exciting +reliability +thongs +unfortunately +respectively +volunteers +attachment +ringtone +finland +morgan +derived +pleasure +honor +oriented +eagle +desktops +pants +columbus +nurse +prayer +appointment +workshops +hurricane +quiet +postage +producer +represented +mortgages +responsibilities +cheese +comic +carefully +productivity +investors +crown +underground +diagnosis +maker +crack +principle +picks +vacations +semester +calculated +fetish +applies +casinos +appearance +smoke +apache +filters +incorporated +craft +notebooks +apart +fellow +blind +lounge +algorithm +coins +gross +strongly +valentine +hilton +proteins +horror +familiar +capable +douglas +debian +involving +investing +christopher +admission +epson +elected +carrying +victory +madison +terrorism +editions +mainly +ethnic +parliament +actor +finds +situations +fifth +allocated +citizen +vertical +corrections +structural +municipal +describes +prize +occurs +absolute +disabilities +consists +anytime +substance +prohibited +addressed +soldiers +guardian +lecture +simulation +layout +initiatives +concentration +classics +interpretation +horses +dirty +wayne +donate +taught +bankruptcy +worker +optimization +alive +temple +substances +prove +discovered +wings +breaks +genetic +restrictions +participating +waters +promise +exhibition +prefer +ridge +cabinet +modem +harris +bringing +evaluate +tiffany +tropical +collect +composition +toyota +streets +nationwide +vector +definitely +shaved +turning +buffer +purple +existence +commentary +larry +limousines +developments +immigration +destinations +mutual +pipeline +necessarily +syntax +attribute +prison +skill +chairs +everyday +apparently +surrounding +mountains +moves +popularity +inquiry +ethernet +checked +exhibit +throw +trend +sierra +visible +desert +postposted +oldest +rhode +coordinator +obviously +mercury +steven +handbook +navigate +worse +summit +victims +spaces +fundamental +burning +escape +coupons +somewhat +receiver +substantial +progressive +cialis +boats +glance +scottish +championship +arcade +richmond +sacramento +impossible +russell +tells +obvious +fiber +depression +graph +covering +platinum +judgment +bedrooms +talks +filing +foster +modeling +passing +awarded +testimonials +trials +tissue +memorabilia +clinton +masters +bonds +cartridge +alberta +explanation +commons +cincinnati +subsection +fraud +electricity +permitted +spectrum +arrival +pottery +emphasis +roger +aspect +workplace +awesome +mexican +confirmed +counts +priced +wallpapers +crash +desired +inter +closer +assumes +heights +shadow +riding +infection +firefox +expense +grove +eligibility +venture +clinic +korean +healing +princess +entering +packet +spray +studios +involvement +buttons +placement +observations +vbulletin +funded +thompson +winners +extend +roads +subsequent +dublin +rolling +motorcycle +disclosure +establishment +memories +nelson +arrived +creates +faces +tourist +mayor +murder +adequate +senator +yield +presentations +grades +cartoons +digest +lodging +hence +entirely +replaced +radar +rescue +undergraduate +losses +combat +reducing +stopped +occupation +lakes +donations +associations +citysearch +closely +radiation +diary +seriously +kings +shooting +flags +baker +launched +elsewhere +pollution +conservative +guestbook +shock +effectiveness +walls +abroad +ebony +drawn +arthur +visited +walker +demonstrate +atmosphere +suggests +beast +operated +experiment +targets +overseas +purchases +dodge +counsel +federation +pizza +invited +yards +assignment +chemicals +gordon +farmers +queries +ukraine +absence +nearest +cluster +vendors +whereas +serves +woods +surprise +partial +shoppers +everybody +couples +nashville +ranking +jokes +simpson +twiki +sublime +counseling +palace +acceptable +satisfied +measurements +verify +globe +trusted +copper +milwaukee +medication +warehouse +shareware +dicke +kerry +receipt +supposed +ordinary +nobody +ghost +violation +configure +stability +applying +southwest +pride +institutional +expectations +independence +knowing +reporter +metabolism +keith +champion +cloudy +linda +personally +chile +plenty +sentence +throat +ignore +maria +uniform +excellence +wealth +somewhere +vacuum +dancing +attributes +recognize +brass +writes +plaza +outcomes +survival +quest +publish +screening +thumbnail +trans +jonathan +whenever +lifetime +pioneer +booty +forgotten +acrobat +plates +acres +venue +athletic +thermal +essays +behaviour +vital +telling +fairly +coastal +config +charity +intelligent +edinburgh +excel +modes +obligation +campbell +stupid +harbor +hungary +traveler +segment +realize +regardless +enemy +puzzle +rising +aluminum +wells +wishlist +opens +insight +restricted +republican +secrets +lucky +latter +merchants +thick +trailers +repeat +syndrome +philips +attendance +penalty +glasses +enables +iraqi +builder +vista +jessica +chips +terry +flood +arguments +amsterdam +arena +adventures +pupils +stewart +announcement +outcome +appreciate +expanded +casual +grown +polish +lovely +extras +centres +jerry +clause +smile +lands +troops +indoor +bulgaria +armed +broker +charger +regularly +believed +cooling +trucks +mechanisms +divorce +laura +shopper +tokyo +partly +nikon +customize +tradition +candy +pills +tiger +donald +folks +sensor +exposed +telecom +angels +deputy +indicators +sealed +emissions +physicians +loaded +complaint +scenes +experiments +afghanistan +boost +spanking +scholarship +governance +founded +supplements +chronic +icons +moral +catering +finger +keeps +pound +locate +camcorder +trained +implementing +roses +ourselves +bread +tobacco +wooden +motors +tough +roberts +incident +gonna +dynamics +conversation +decrease +chest +pension +billy +revenues +emerging +worship +capability +craig +herself +producing +churches +precision +damages +reserves +contributed +solve +shorts +reproduction +minority +diverse +ingredients +johnny +franchise +recorder +complaints +facing +nancy +promotions +tones +passion +rehabilitation +maintaining +sight +defence +patches +refund +towns +environments +trembl +divided +reception +emails +cyprus +correctly +insider +seminars +consequences +makers +hearts +geography +appearing +integrity +worry +discrimination +carter +legacy +pleased +danger +vitamin +widely +processed +phrase +genuine +raising +implications +functionality +paradise +hybrid +reads +roles +intermediate +emotional +glory +platforms +bigger +billing +diesel +versus +combine +overnight +geographic +exceed +saudi +fault +preliminary +districts +introduce +promotional +chevrolet +babies +karen +compiled +romantic +revealed +specialists +generator +albert +examine +jimmy +graham +suspension +bristol +margaret +compaq +correction +slowly +authentication +communicate +rugby +supplement +showtimes +portions +infant +promoting +sectors +samuel +fluid +grounds +regards +machinery +bandwidth +unlike +equation +baskets +probability +dimension +wright +barry +proven +schedules +admissions +cached +warren +studied +reviewer +involves +quarterly +profits +devil +grass +comply +marie +florist +illustrated +cherry +continental +alternate +deutsch +achievement +limitations +kenya +webcam +funeral +nutten +earrings +enjoyed +automated +chapters +charlie +quebec +passenger +convenient +dennis +francis +sized +manga +noticed +socket +silent +literary +signals +orientation +theft +childhood +swing +symbols +humans +analog +facial +choosing +talent +dated +flexibility +seeker +wisdom +shoot +boundary +packard +offset +payday +philip +elite +holders +believes +swedish +poems +deadline +jurisdiction +robot +displaying +witness +collins +equipped +stages +encouraged +winds +powder +broadway +acquired +assess +cartridges +stones +entrance +gnome +roots +declaration +losing +attempts +gadgets +noble +glasgow +automation +impacts +gospel +advantages +shore +loves +induced +knight +preparing +loose +recipient +linking +extensions +appeals +earned +illness +islamic +athletics +southeast +alternatives +pending +parker +determining +lebanon +personalized +kennedy +conditioning +teenage +triple +cooper +vincent +secured +unusual +answered +partnerships +destruction +slots +increasingly +migration +disorder +routine +toolbar +basically +rocks +conventional +titans +applicants +wearing +sought +genes +mounted +habitat +firewall +median +scanner +herein +occupational +animated +judicial +adjustment +integer +treatments +bachelor +attitude +camcorders +engaged +falling +basics +montreal +carpet +struct +lenses +binary +genetics +attended +difficulty +collective +coalition +dropped +enrollment +walter +besides +producers +collector +hosts +interfaces +advertisers +moments +atlas +strings +representing +observation +feels +torture +deleted +mitchell +restoration +convenience +returning +ralph +opposition +container +defendant +warner +confirmation +embedded +inkjet +supervisor +wizard +corps +actors +liver +peripherals +liable +brochure +morris +bestsellers +petition +eminem +recall +antenna +picked +assumed +departure +minneapolis +belief +killing +bikini +memphis +shoulder +decor +lookup +texts +harvard +brokers +diameter +ottawa +podcast +seasons +interactions +refine +bidder +singer +evans +herald +literacy +fails +aging +intervention +plugin +attraction +diving +invite +modification +alice +latinas +suppose +customized +involve +moderate +terror +younger +thirty +opposite +understood +rapidly +dealtime +intro +mercedes +assurance +clerk +happening +mills +outline +amendments +tramadol +holland +receives +jeans +metropolitan +compilation +verification +fonts +refers +favor +veterans +sigma +attractive +xhtml +occasion +recordings +jefferson +victim +demands +sleeping +careful +gardening +obligations +arrive +orchestra +sunset +tracked +moreover +minimal +polyphonic +lottery +framed +aside +outsourcing +licence +adjustable +allocation +michelle +essay +discipline +demonstrated +dialogue +identifying +alphabetical +camps +declared +dispatched +aaron +handheld +trace +disposal +florists +packs +installing +switches +romania +voluntary +consult +greatly +blogging +cycling +midnight +commonly +photographer +inform +turkish +messaging +pentium +quantum +murray +intent +largely +pleasant +announce +constructed +additions +requiring +spoke +arrow +engagement +sampling +rough +weird +refinance +inspired +holes +weddings +blade +suddenly +oxygen +cookie +meals +canyon +meters +merely +calendars +arrangement +conclusions +passes +bibliography +pointer +compatibility +stretch +durham +furthermore +permits +cooperative +muslim +sleeve +netscape +cleaner +cricket +feeding +stroke +township +rankings +measuring +robin +robinson +jacksonville +strap +headquarters +sharon +crowd +transfers +olympic +transformation +remained +attachments +entities +customs +administrators +personality +rainbow +roulette +decline +gloves +israeli +medicare +skiing +cloud +facilitate +subscriber +valve +hewlett +explains +proceed +flickr +feelings +knife +jamaica +priorities +shelf +bookstore +timing +liked +parenting +adopt +denied +fotos +incredible +britney +freeware +donation +outer +deaths +rivers +commonwealth +pharmaceutical +manhattan +tales +katrina +workforce +islam +nodes +thumbs +seeds +cited +targeted +organizational +skype +realized +twelve +founder +decade +gamecube +dispute +portuguese +tired +titten +adverse +everywhere +excerpt +steam +discharge +drinks +voices +acute +halloween +climbing +stood +perfume +carol +honest +albany +hazardous +restore +stack +methodology +somebody +housewares +reputation +resistant +democrats +recycling +curve +creator +amber +qualifications +museums +coding +slideshow +tracker +variation +passage +transferred +trunk +hiking +pierre +jelsoft +headset +photograph +oakland +colombia +waves +camel +distributor +lamps +underlying +wrestling +suicide +archived +photoshop +arabia +gathering +projection +juice +chase +mathematical +logical +sauce +extract +specialized +diagnostic +panama +indianapolis +payable +corporations +courtesy +criticism +automobile +confidential +statutory +accommodations +athens +northeast +downloaded +judges +retired +remarks +detected +decades +paintings +walked +arising +nissan +bracelet +juvenile +injection +yorkshire +populations +protective +afraid +acoustic +railway +cassette +initially +indicator +pointed +causing +mistake +norton +locked +eliminate +fusion +mineral +sunglasses +steering +beads +fortune +preference +canvas +threshold +parish +claimed +screens +cemetery +planner +croatia +flows +stadium +venezuela +exploration +fewer +sequences +coupon +nurses +proxy +astronomy +lanka +edwards +contests +translate +announces +costume +tagged +berkeley +voted +killer +bikes +gates +adjusted +bishop +pulled +shaped +compression +seasonal +establishing +farmer +counters +constitutional +perfectly +slave +instantly +cultures +norfolk +coaching +examined +encoding +litigation +submissions +heroes +painted +lycos +zdnet +broadcasting +horizontal +artwork +cosmetic +resulted +portrait +terrorist +informational +ethical +carriers +ecommerce +mobility +floral +builders +struggle +schemes +suffering +neutral +fisher +spears +prospective +bedding +ultimately +joining +heading +equally +artificial +bearing +spectacular +coordination +connector +combo +seniors +worlds +guilty +affiliated +activation +naturally +haven +tablet +subscribers +charm +violent +mitsubishi +underwear +basin +potentially +ranch +constraints +crossing +inclusive +dimensional +cottage +drunk +considerable +crimes +resolved +mozilla +toner +latex +branches +anymore +delhi +holdings +alien +locator +selecting +processors +pantyhose +broke +nepal +zimbabwe +difficulties +complexity +constantly +browsing +resolve +barcelona +presidential +documentary +territories +melissa +moscow +thesis +nylon +palestinian +discs +rocky +bargains +frequent +nigeria +ceiling +pixels +ensuring +hispanic +legislature +hospitality +anybody +procurement +diamonds +fleet +untitled +bunch +totals +marriott +singing +theoretical +afford +exercises +starring +referral +surveillance +optimal +distinct +protocols +highlight +substitute +inclusion +hopefully +brilliant +turner +sucking +cents +reuters +spoken +omega +evaluated +stayed +civic +assignments +manuals +termination +watched +saver +thereof +grill +households +redeem +rogers +grain +authentic +regime +wanna +wishes +montgomery +architectural +louisville +depend +differ +macintosh +movements +ranging +monica +repairs +breath +amenities +virtually +candle +hanging +colored +authorization +verified +formerly +projector +situated +comparative +seeks +herbal +loving +strictly +routing +stanley +psychological +surprised +retailer +vitamins +elegant +gains +renewal +genealogy +opposed +deemed +scoring +expenditure +brooklyn +liverpool +sisters +critics +connectivity +spots +algorithms +hacker +madrid +similarly +margin +solely +salon +collaborative +norman +excluding +turbo +headed +voters +madonna +commander +murphy +thinks +thats +suggestion +soldier +phillips +aimed +justin +interval +mirrors +spotlight +tricks +reset +brush +investigate +expansys +panels +repeated +assault +connecting +spare +logistics +kodak +tongue +bowling +danish +monkey +proportion +filename +skirt +florence +invest +honey +analyses +drawings +significance +scenario +lovers +atomic +approx +symposium +arabic +gauge +essentials +junction +protecting +faced +rachel +solving +transmitted +weekends +screenshots +produces +intensive +chains +kingston +sixth +engage +deviant +switching +quoted +adapters +correspondence +farms +imports +supervision +cheat +bronze +expenditures +sandy +separation +testimony +suspect +celebrities +macro +sender +mandatory +boundaries +crucial +syndication +celebration +adjacent +filtering +tuition +spouse +exotic +viewer +signup +threats +luxembourg +puzzles +reaching +damaged +receptor +laugh +surgical +destroy +citation +pitch +autos +premises +perry +proved +offensive +imperial +dozen +benjamin +deployment +teeth +cloth +studying +colleagues +stamp +lotus +salmon +olympus +separated +cargo +directive +salem +starter +upgrades +likes +butter +pepper +weapon +luggage +burden +tapes +zones +races +stylish +maple +grocery +offshore +governing +retailers +depot +kenneth +blend +harrison +julie +occasionally +attending +emission +finest +realty +janet +recruiting +apparent +instructional +phpbb +autumn +traveling +probe +permissions +biotechnology +toilet +ranked +jackets +routes +packed +excited +outreach +helen +mounting +recover +lopez +balanced +prescribed +catherine +timely +talked +debug +delayed +chuck +reproduced +explicit +calculation +villas +ebook +consolidated +exclude +peeing +occasions +brooks +equations +newton +exceptional +anxiety +bingo +whilst +spatial +respondents +ceramic +prompt +precious +minds +annually +considerations +scanners +xanax +fingers +sunny +ebooks +delivers +queensland +necklace +musicians +leeds +composite +unavailable +cedar +arranged +theaters +advocacy +raleigh +essentially +designing +threaded +qualify +blair +hopes +assessments +mason +diagram +burns +pumps +footwear +beijing +peoples +victor +mario +attach +licenses +utils +removing +advised +brunswick +spider +ranges +pairs +sensitivity +trails +preservation +hudson +isolated +calgary +interim +assisted +divine +streaming +approve +chose +compound +intensity +technological +syndicate +abortion +dialog +venues +blast +wellness +calcium +newport +antivirus +addressing +discounted +indians +shield +harvest +membrane +prague +previews +bangladesh +constitute +locally +concluded +pickup +desperate +mothers +nascar +iceland +demonstration +governmental +manufactured +candles +graduation +sailing +variations +sacred +addiction +morocco +chrome +tommy +springfield +refused +brake +exterior +greeting +ecology +oliver +congo +botswana +delays +synthesis +olive +undefined +unemployment +cyber +verizon +scored +enhancement +newcastle +clone +dicks +velocity +lambda +relay +composed +tears +performances +oasis +baseline +angry +societies +silicon +brazilian +identical +petroleum +compete +norwegian +lover +belong +honolulu +beatles +retention +exchanges +rolls +thomson +barnes +soundtrack +wondering +malta +daddy +ferry +rabbit +profession +seating +separately +physiology +collecting +exports +omaha +participant +scholarships +recreational +dominican +electron +loads +friendship +heather +passport +motel +unions +treasury +warrant +solaris +frozen +occupied +royalty +scales +rally +observer +sunshine +strain +ceremony +somehow +arrested +expanding +provincial +investigations +yamaha +medications +hebrew +gained +rochester +dying +laundry +stuck +solomon +placing +stops +homework +adjust +assessed +advertiser +enabling +encryption +filling +downloadable +sophisticated +imposed +silence +focuses +soviet +possession +laboratories +treaty +vocal +trainer +organ +stronger +volumes +advances +vegetables +lemon +toxic +thumbnails +darkness +bizrate +vienna +implied +stanford +stockings +respondent +packing +statute +rejected +satisfy +destroyed +shelter +chapel +gamespot +manufacture +layers +wordpress +guided +vulnerability +accountability +celebrate +accredited +appliance +compressed +bahamas +powell +mixture +bench +rider +scheduling +radius +perspectives +mortality +logging +hampton +christians +borders +therapeutic +butts +bobby +impressive +sheep +accordingly +architect +railroad +lectures +challenging +wines +nursery +harder +microwave +cheapest +accidents +travesti +relocation +stuart +contributors +salvador +salad +monroe +tender +violations +temperatures +paste +clouds +competitions +discretion +tanzania +preserve +unsigned +staying +cosmetics +easter +theories +repository +praise +jeremy +venice +concentrations +estonia +christianity +veteran +streams +landing +signing +executed +katie +negotiations +realistic +showcase +integral +relax +namibia +generating +christina +congressional +synopsis +hardly +prairie +reunion +composer +sword +absent +photographic +sells +ecuador +hoping +accessed +spirits +modifications +coral +pixel +float +colin +imported +paths +bubble +acquire +contrary +millennium +tribune +vessel +acids +focusing +viruses +cheaper +admitted +dairy +admit +fancy +equality +samoa +achieving +stickers +fisheries +exceptions +reactions +leasing +lauren +beliefs +macromedia +companion +squad +analyze +ashley +scroll +relate +divisions +wages +additionally +suffer +forests +fellowship +invalid +concerts +martial +males +victorian +retain +colours +execute +tunnel +genres +cambodia +patents +copyrights +chaos +lithuania +mastercard +wheat +chronicles +obtaining +beaver +updating +distribute +readings +decorative +kijiji +confused +compiler +enlargement +eagles +bases +accused +campaigns +unity +conjunction +bride +defines +airports +instances +indigenous +begun +brunette +packets +anchor +socks +validation +parade +corruption +trigger +incentives +cholesterol +gathered +essex +slovenia +notified +differential +beaches +folders +dramatic +surfaces +terrible +routers +pendant +dresses +baptist +scientist +starsmerchant +hiring +clocks +arthritis +females +wallace +nevertheless +reflects +taxation +fever +cuisine +surely +practitioners +transcript +myspace +theorem +inflation +stylus +compounds +drums +contracting +arnold +structured +reasonably +chicks +cattle +radical +graduates +rover +recommends +controlling +treasure +reload +distributors +flame +levitra +tanks +assuming +monetary +elderly +arlington +particles +floating +extraordinary +indicating +bolivia +spell +hottest +stevens +coordinate +kuwait +exclusively +emily +alleged +limitation +widescreen +compile +webster +struck +illustration +plymouth +warnings +construct +inquiries +bridal +annex +inspiration +tribal +curious +affecting +freight +rebate +meetup +eclipse +sudan +downloading +shuttle +aggregate +stunning +cycles +affects +forecasts +detect +actively +ampland +complicated +fastest +butler +shopzilla +injured +decorating +payroll +cookbook +expressions +courier +uploaded +shakespeare +hints +collapse +americas +connectors +unlikely +conflicts +techno +beverage +tribute +wired +elvis +immune +latvia +travelers +forestry +barriers +rarely +infected +offerings +martha +genesis +barrier +argue +incorrect +trains +metals +bicycle +furnishings +letting +arise +guatemala +celtic +thereby +jamie +particle +perception +minerals +advise +humidity +bottles +boxing +bangkok +renaissance +pathology +ordinance +hughes +photographers +infections +jeffrey +chess +operates +brisbane +configured +survive +oscar +festivals +menus +possibilities +reveal +canal +amino +contributing +herbs +clinics +manitoba +analytical +missions +watson +lying +costumes +strict +saddam +circulation +drill +offense +bryan +protest +assumption +jerusalem +hobby +tries +transexuales +invention +nickname +technician +inline +executives +enquiries +washing +staffing +cognitive +exploring +trick +enquiry +closure +timber +intense +playlist +registrar +showers +supporters +ruling +steady +statutes +withdrawal +myers +drops +predicted +wider +saskatchewan +cancellation +plugins +enrolled +sensors +screw +ministers +publicly +hourly +blame +geneva +freebsd +veterinary +prostores +reseller +handed +suffered +intake +informal +relevance +incentive +butterfly +tucson +mechanics +heavily +swingers +fifty +headers +mistakes +numerical +uncle +defining +counting +reflection +accompanied +assure +invitation +devoted +princeton +jacob +sodium +randy +spirituality +hormone +meanwhile +proprietary +timothy +childrens +brick +naval +thumbzilla +medieval +porcelain +bridges +pichunter +captured +thehun +decent +casting +dayton +translated +shortly +cameron +columnists +carlos +donna +andreas +warrior +diploma +cabin +innocent +scanning +consensus +valium +copying +delivering +cordless +patricia +eddie +uganda +fired +journalism +trivia +adidas +perth +grammar +intention +syria +disagree +klein +harvey +tires +undertaken +hazard +retro +statewide +semiconductor +gregory +episodes +boolean +circular +anger +mainland +illustrations +suits +chances +interact +happiness +substantially +bizarre +glenn +auckland +olympics +fruits +identifier +ribbon +calculations +conducting +startup +suzuki +trinidad +kissing +handy +exempt +crops +reduces +accomplished +calculators +geometry +impression +slovakia +guild +correlation +gorgeous +capitol +dishes +barbados +chrysler +nervous +refuse +extends +fragrance +mcdonald +replica +plumbing +brussels +tribe +neighbors +trades +superb +transparent +trinity +charleston +handled +legends +champions +floors +selections +projectors +inappropriate +exhaust +comparing +shanghai +speaks +burton +vocational +davidson +copied +scotia +farming +gibson +pharmacies +roller +introducing +batch +organize +appreciated +alter +nicole +latino +ghana +edges +mixing +handles +skilled +fitted +albuquerque +harmony +distinguished +asthma +projected +assumptions +shareholders +twins +developmental +regulated +triangle +amend +anticipated +oriental +reward +windsor +zambia +completing +hydrogen +webshots +sprint +comparable +chick +advocate +confusion +copyrighted +inputs +warranties +genome +escorts +documented +thong +medal +paperbacks +coaches +vessels +harbour +walks +keyboards +knives +vulnerable +arrange +artistic +honors +booth +indie +reflected +unified +bones +breed +detector +ignored +polar +fallen +precise +sussex +respiratory +notifications +msgid +transexual +mainstream +invoice +evaluating +subcommittee +gather +maternity +backed +alfred +colonial +carey +motels +forming +embassy +journalists +danny +rebecca +slight +proceeds +indirect +amongst +foundations +msgstr +arrest +volleyball +adipex +horizon +deeply +toolbox +marina +liabilities +prizes +bosnia +browsers +decreased +patio +tolerance +surfing +creativity +lloyd +describing +optics +pursue +lightning +overcome +quotations +inspector +attract +brighton +beans +bookmarks +ellis +disable +snake +succeed +leonard +lending +reminder +searched +behavioral +riverside +bathrooms +plains +raymond +insights +abilities +initiated +sullivan +midwest +karaoke +lonely +nonprofit +lancaster +suspended +hereby +observe +julia +containers +attitudes +berry +collar +simultaneously +racial +integrate +bermuda +amanda +sociology +mobiles +screenshot +exhibitions +kelkoo +confident +retrieved +exhibits +officially +consortium +terrace +bacteria +replied +seafood +novels +recipients +ought +delicious +traditions +safely +finite +kidney +periodically +fixes +sends +durable +mazda +allied +throws +moisture +hungarian +roster +referring +symantec +spencer +wichita +nasdaq +uruguay +transform +timer +tablets +tuning +gotten +educators +tyler +futures +vegetable +verse +highs +humanities +independently +wanting +custody +scratch +launches +alignment +masturbating +henderson +britannica +ellen +competitors +rocket +bullet +towers +racks +nasty +visibility +latitude +consciousness +tumor +deposits +beverly +mistress +encounter +trustees +watts +duncan +reprints +bernard +resolutions +accessing +forty +tubes +attempted +midlands +priest +floyd +ronald +analysts +queue +trance +locale +nicholas +bundle +hammer +invasion +witnesses +runner +administered +notion +skins +mailed +fujitsu +spelling +arctic +exams +rewards +beneath +strengthen +defend +frederick +medicaid +infrared +seventh +welsh +belly +aggressive +advertisements +quarters +stolen +soonest +haiti +disturbed +determines +sculpture +naturals +motivation +lenders +pharmacology +fitting +fixtures +bloggers +agrees +passengers +quantities +petersburg +consistently +powerpoint +surplus +elder +sonic +obituaries +cheers +punishment +appreciation +subsequently +belarus +zoning +gravity +providence +thumb +restriction +incorporate +backgrounds +treasurer +guitars +essence +flooring +lightweight +ethiopia +mighty +athletes +humanity +transcription +holmes +complications +scholars +scripting +remembered +galaxy +chester +snapshot +caring +synthetic +segments +testament +dominant +twist +specifics +itunes +stomach +partially +buried +newbie +minimize +darwin +ranks +wilderness +debut +generations +tournaments +bradley +anatomy +sponsorship +headphones +fraction +proceeding +defects +volkswagen +uncertainty +breakdown +milton +marker +reconstruction +subsidiary +strengths +clarity +sandra +adelaide +encouraging +furnished +monaco +settled +folding +emirates +terrorists +airfare +comparisons +beneficial +distributions +vaccine +belize +viewpicture +promised +volvo +penny +robust +bookings +threatened +minolta +republicans +discusses +porter +jungle +responded +abstracts +ivory +alpine +prediction +pharmaceuticals +andale +fabulous +remix +alias +thesaurus +individually +battlefield +literally +newer +ecological +spice +implies +cooler +appraisal +consisting +maritime +periodic +submitting +overhead +ascii +prospect +shipment +breeding +citations +geographical +donor +mozambique +tension +trash +shapes +manor +envelope +diane +homeland +disclaimers +championships +excluded +andrea +breeds +rapids +disco +sheffield +bailey +endif +finishing +emotions +wellington +incoming +prospects +lexmark +cleaners +bulgarian +eternal +cashiers +aboriginal +remarkable +rotation +preventing +productive +boulevard +eugene +metric +compliant +minus +penalties +bennett +imagination +hotmail +refurbished +joshua +armenia +varied +grande +closest +activated +actress +conferencing +assign +armstrong +politicians +trackbacks +accommodate +tigers +aurora +slides +milan +premiere +lender +villages +shade +chorus +christine +rhythm +digit +argued +dietary +symphony +clarke +sudden +accepting +precipitation +marilyn +lions +findlaw +pools +lyric +claire +isolation +speeds +sustained +matched +approximate +carroll +rational +programmer +fighters +chambers +greetings +inherited +warming +incomplete +vocals +chronicle +fountain +chubby +grave +legitimate +biographies +burner +investigator +plaintiff +finnish +gentle +prisoners +deeper +muslims +mediterranean +nightlife +footage +howto +worthy +reveals +architects +saints +entrepreneur +carries +freelance +excessive +devon +screensaver +helena +saves +regarded +valuation +unexpected +cigarette +characteristic +marion +lobby +egyptian +tunisia +metallica +outlined +consequently +headline +treating +punch +appointments +gotta +cowboy +narrative +bahrain +enormous +karma +consist +betty +queens +academics +quantitative +lucas +screensavers +subdivision +tribes +defeat +clicks +distinction +honduras +naughty +hazards +insured +harper +livestock +mardi +exemption +tenant +sustainability +cabinets +tattoo +shake +algebra +shadows +holly +formatting +silly +nutritional +mercy +hartford +freely +marcus +sunrise +wrapping +nicaragua +weblogs +timeline +belongs +readily +affiliation +fence +nudist +infinite +diana +ensures +relatives +lindsay +legally +shame +satisfactory +revolutionary +bracelets +civilian +telephony +fatal +remedy +realtors +breathing +briefly +thickness +adjustments +graphical +genius +discussing +aerospace +fighter +meaningful +flesh +retreat +adapted +barely +wherever +estates +democrat +borough +maintains +failing +shortcuts +retained +voyeurweb +pamela +andrews +marble +extending +jesse +specifies +logitech +surrey +briefing +belkin +accreditation +blackberry +highland +meditation +modular +microphone +macedonia +combining +brandon +instrumental +giants +organizing +balloon +moderators +winston +solved +kazakhstan +hawaiian +standings +partition +invisible +gratuit +consoles +qatar +magnet +translations +porsche +cayman +jaguar +sheer +commodity +posing +kilometers +thanksgiving +hopkins +urgent +guarantees +infants +gothic +cylinder +witch +indication +congratulations +cohen +puppy +kathy +graphs +surround +cigarettes +revenge +expires +enemies +controllers +consultancy +finances +accepts +enjoying +conventions +patrol +smell +italiano +coordinates +carnival +roughly +sticker +promises +responding +physically +divide +stakeholders +hydrocodone +consecutive +cornell +satin +deserve +attempting +mailto +promo +representations +worried +tunes +garbage +competing +combines +bradford +phrases +peninsula +chelsea +boring +reynolds +accurately +speeches +reaches +schema +considers +catalogs +ministries +vacancies +quizzes +parliamentary +prefix +lucia +savannah +barrel +typing +nerve +planets +deficit +boulder +pointing +renew +coupled +myanmar +metadata +harold +circuits +floppy +texture +handbags +somerset +incurred +acknowledge +thoroughly +antigua +nottingham +thunder +caution +identifies +questionnaire +qualification +locks +modelling +namely +miniature +euros +interstate +pirates +aerial +consequence +rebel +systematic +perceived +origins +hired +makeup +textile +madagascar +nathan +tobago +presenting +troubleshooting +uzbekistan +indexes +centuries +magnitude +richardson +hindu +fragrances +vocabulary +licking +earthquake +fundraising +markers +weights +albania +geological +assessing +lasting +wicked +introduces +kills +roommate +webcams +pushed +webmasters +computational +acdbentity +participated +handhelds +answering +impressed +slope +reggae +failures +conspiracy +surname +theology +nails +evident +whats +rides +rehab +saturn +organizer +allergy +twisted +combinations +preceding +merit +enzyme +cumulative +zshops +planes +edmonton +tackle +disks +condo +pokemon +amplifier +ambien +arbitrary +prominent +retrieve +lexington +vernon +worldcat +titanium +fairy +builds +contacted +shaft +recorders +occasional +leslie +casio +deutsche +postings +innovations +kitty +postcards +drain +monte +fires +algeria +blessed +reviewing +cardiff +cornwall +favors +potato +panic +explicitly +sticks +leone +transsexual +citizenship +excuse +reforms +basement +onion +strand +sandwich +lawsuit +informative +girlfriend +bloomberg +cheque +hierarchy +influenced +banners +reject +abandoned +circles +italic +beats +merry +scuba +complement +passive +mauritius +valued +checklist +requesting +courage +verde +lauderdale +scenarios +gazette +hitachi +extraction +batman +elevation +hearings +coleman +utilization +beverages +calibration +efficiently +anaheim +textbook +dried +entertaining +prerequisite +luther +frontier +settle +stopping +refugees +knights +hypothesis +palmer +medicines +derby +peaceful +altered +pontiac +regression +doctrine +scenic +trainers +enhancements +renewable +intersection +passwords +sewing +consistency +collectors +conclude +recognised +munich +celebs +propose +azerbaijan +lighter +astrology +advisors +pavilion +tactics +trusts +occurring +supplemental +travelling +talented +annie +pillow +induction +derek +precisely +shorter +harley +spreading +provinces +relying +finals +paraguay +steal +parcel +refined +fifteen +widespread +incidence +fears +predict +boutique +acrylic +rolled +tuner +incidents +peterson +shannon +toddler +enhancing +flavor +alike +homeless +horrible +hungry +metallic +blocked +interference +warriors +palestine +listprice +cadillac +atmospheric +malawi +sagem +knowledgestorm +curtis +parental +referenced +strikes +lesser +publicity +marathon +proposition +pressing +gasoline +dressed +scout +belfast +dealt +niagara +warcraft +charms +catalyst +trader +bucks +allowance +denial +designation +thrown +prepaid +raises +duplicate +electro +criterion +badge +wrist +civilization +analyzed +vietnamese +heath +tremendous +ballot +lexus +varying +remedies +validity +trustee +weighted +angola +performs +plastics +realm +corrected +jenny +helmet +salaries +postcard +elephant +yemen +encountered +tsunami +scholar +nickel +internationally +surrounded +buses +expedia +geology +creatures +coating +commented +wallet +cleared +smilies +accomplish +boating +drainage +shakira +corners +broader +vegetarian +rouge +yeast +newfoundland +clearing +investigated +ambassador +coated +intend +stephanie +contacting +vegetation +findarticles +louise +kenny +specially +routines +hitting +yukon +beings +aquatic +reliance +habits +striking +infectious +podcasts +singh +gilbert +ferrari +continuity +brook +outputs +phenomenon +ensemble +insulin +assured +biblical +conscious +accent +mysimon +eleven +wives +ambient +utilize +mileage +prostate +adaptor +auburn +unlock +hyundai +pledge +vampire +angela +relates +nitrogen +xerox +merger +softball +referrals +differently +firewire +nextel +framing +organised +musician +blocking +rwanda +sorts +integrating +vsnet +limiting +dispatch +revisions +papua +restored +armor +riders +chargers +remark +dozens +varies +reasoning +rendered +picking +charitable +guards +annotated +convinced +openings +burlington +replacing +researcher +watershed +councils +occupations +acknowledged +kruger +pockets +granny +equilibrium +viral +inquire +pipes +characterized +laden +aruba +cottages +realtor +merge +privilege +edgar +develops +qualifying +chassis +dubai +estimation +pushing +fleece +pediatric +pierce +allan +dressing +techrepublic +sperm +filme +craps +frost +institutes +sally +yacht +tracy +prefers +drilling +brochures +breach +whale +traveller +appropriations +suspected +tomatoes +benchmark +beginners +instructors +highlighted +bedford +stationery +mustang +unauthorized +clusters +antibody +competent +momentum +wiring +pastor +calvin +shark +contributor +demonstrates +phases +grateful +emerald +gradually +laughing +grows +cliff +desirable +tract +ballet +journalist +abraham +bumper +afterwards +webpage +religions +garlic +hostels +shine +senegal +explosion +banned +wendy +briefs +signatures +diffs +mumbai +ozone +disciplines +daughters +conversations +radios +tariff +nvidia +opponent +pasta +simplified +muscles +serum +wrapped +swift +motherboard +runtime +inbox +focal +bibliographic +distant +champagne +decimal +deviation +superintendent +propecia +samba +hostel +housewives +employ +mongolia +penguin +magical +influences +inspections +irrigation +miracle +manually +reprint +hydraulic +centered +robertson +yearly +penetration +wound +belle +conviction +omissions +writings +hamburg +retrieval +qualities +cindy +fathers +charging +marvel +lined +prototype +importantly +petite +apparatus +terrain +explaining +strips +gossip +rangers +nomination +empirical +rotary +dependence +discrete +beginner +boxed +sexuality +polyester +cubic +commitments +suggesting +sapphire +kinase +skirts +remainder +crawford +labeled +privileges +televisions +specializing +marking +commodities +serbia +sheriff +griffin +declined +guyana +spies +neighbor +motorcycles +elect +highways +thinkpad +concentrate +intimate +reproductive +preston +deadly +bunny +chevy +molecules +rounds +longest +refrigerator +tions +intervals +sentences +dentists +exclusion +workstation +holocaust +flyer +dosage +receivers +customise +disposition +variance +navigator +investigators +cameroon +baking +marijuana +adaptive +computed +needle +baths +cathedral +brakes +nirvana +fairfield +invision +sticky +destiny +generous +madness +emacs +climb +blowing +fascinating +landscapes +heated +lafayette +jackie +computation +cardiovascular +sparc +cardiac +salvation +dover +adrian +predictions +accompanying +vatican +brutal +learners +selective +arbitration +configuring +token +editorials +sacrifice +seekers +removable +convergence +yields +gibraltar +suited +numeric +anthropology +skating +kinda +aberdeen +emperor +malpractice +dylan +belts +blacks +educated +rebates +reporters +burke +proudly +necessity +rendering +inserted +pulling +basename +obesity +curves +suburban +touring +clara +vertex +hepatitis +nationally +tomato +andorra +waterproof +expired +travels +flush +waiver +specialties +hayes +humanitarian +invitations +functioning +delight +survivor +garcia +cingular +economies +alexandria +bacterial +moses +counted +undertake +declare +continuously +johns +valves +impaired +achievements +donors +jewel +teddy +convertible +teaches +ventures +bufing +stranger +tragedy +julian +dryer +painful +velvet +tribunal +ruled +pensions +prayers +funky +secretariat +nowhere +paragraphs +joins +adolescent +nominations +wesley +lately +cancelled +scary +mattress +mpegs +brunei +likewise +banana +introductory +slovak +cakes +reservoir +occurrence +mixer +remind +worcester +sbjct +demographic +charming +tooth +disciplinary +annoying +respected +stays +disclose +affair +drove +washer +upset +restrict +springer +beside +mines +portraits +rebound +logan +mentor +interpreted +evaluations +fought +baghdad +elimination +metres +hypothetical +immigrants +complimentary +helicopter +pencil +freeze +performer +titled +commissions +sphere +powerseller +ratios +concord +graduated +endorsed +surprising +walnut +lance +ladder +italia +unnecessary +dramatically +liberia +sherman +maximize +hansen +senators +workout +yugoslavia +bleeding +characterization +colon +likelihood +lanes +purse +fundamentals +contamination +endangered +compromise +masturbation +optimize +stating +caroline +expiration +namespace +align +peripheral +bless +engaging +negotiation +crest +opponents +triumph +nominated +confidentiality +electoral +changelog +welding +deferred +alternatively +alloy +condos +plots +polished +gently +greensboro +tulsa +locking +casey +controversial +draws +fridge +blanket +bloom +simpsons +elliott +recovered +fraser +justify +upgrading +blades +loops +surge +frontpage +trauma +tahoe +advert +possess +demanding +defensive +flashers +subaru +forbidden +vanilla +programmers +monitored +installations +deutschland +picnic +souls +arrivals +spank +practitioner +motivated +smithsonian +hollow +vault +securely +examining +fioricet +groove +revelation +pursuit +delegation +wires +dictionaries +mails +backing +greenhouse +sleeps +blake +transparency +travis +endless +figured +orbit +currencies +niger +bacon +survivors +positioning +heater +colony +cannon +circus +promoted +forbes +moldova +descending +paxil +spine +trout +enclosed +temporarily +cooked +thriller +transmit +apnic +fatty +gerald +pressed +frequencies +scanned +reflections +hunger +mariah +municipality +joyce +detective +surgeon +cement +experiencing +fireplace +endorsement +planners +disputes +textiles +missile +intranet +closes +psychiatry +persistent +deborah +marco +assists +summaries +gabriel +auditor +aquarium +violin +prophet +bracket +looksmart +isaac +oxide +magnificent +colleague +naples +promptly +modems +adaptation +harmful +paintball +prozac +sexually +enclosure +dividend +newark +glucose +phantom +playback +supervisors +westminster +turtle +distances +absorption +treasures +warned +neural +fossil +hometown +badly +transcripts +apollo +disappointed +persian +continually +communist +collectible +handmade +greene +entrepreneurs +robots +grenada +creations +scoop +acquisitions +earning +mailman +sanyo +nested +biodiversity +excitement +somalia +movers +verbal +blink +presently +carlo +workflow +mysterious +novelty +bryant +tiles +voyuer +librarian +subsidiaries +switched +stockholm +tamil +garmin +fuzzy +indonesian +grams +therapist +richards +budgets +toolkit +promising +relaxation +render +carmen +thereafter +hardwood +erotica +temporal +forge +commissioners +dense +brave +forwarding +awful +nightmare +airplane +reductions +southampton +istanbul +impose +organisms +telescope +viewers +asbestos +portsmouth +meyer +enters +savage +advancement +harassment +willow +resumes +throwing +existed +generators +wagon +barbie +favour +knock +generates +potatoes +thorough +replication +inexpensive +receptors +peers +roland +optimum +interventions +quilt +huntington +creature +mounts +syracuse +internship +refresh +aluminium +snowboard +beastality +webcast +michel +evanescence +subtle +coordinated +notre +shipments +maldives +stripes +firmware +antarctica +shepherd +canberra +cradle +chancellor +mambo +flour +controversy +legendary +sympathy +choir +avoiding +beautifully +blond +expects +jumping +fabrics +antibodies +polymer +hygiene +poultry +virtue +burst +examinations +surgeons +bouquet +immunology +promotes +mandate +wiley +departmental +corpus +johnston +terminology +gentleman +fibre +reproduce +convicted +shades +indices +roommates +adware +threatening +spokesman +zoloft +activists +frankfurt +prisoner +daisy +halifax +encourages +ultram +cursor +assembled +earliest +donated +stuffed +restructuring +insects +terminals +crude +morrison +maiden +simulations +sufficiently +examines +viking +myrtle +bored +cleanup +conditional +crossword +bother +budapest +conceptual +knitting +attacked +bhutan +liechtenstein +mating +compute +redhead +arrives +translator +automobiles +tractor +allah +continent +unwrap +fares +longitude +resist +challenged +telecharger +hoped +safer +insertion +instrumentation +wagner +constraint +groundwater +touched +strengthening +cologne +wishing +ranger +smallest +insulation +newman +marsh +ricky +scared +theta +infringement +subjective +monsters +asylum +lightbox +robbie +stake +cocktail +outlets +swaziland +varieties +arbor +mediawiki +configurations +poison diff --git a/devel/devel-server.py b/devel/devel-server.py new file mode 100755 index 0000000..e4b2717 --- /dev/null +++ b/devel/devel-server.py @@ -0,0 +1,287 @@ +#!/usr/bin/python3 + +import asyncio +import cgitb +import html +import cgi +import http.server +import io +import json +import mimetypes +import moth +import logging +import os +import pathlib +import random +import shutil +import socketserver +import sys +import traceback +import mothballer +import parse +import urllib.parse +import posixpath +from http import HTTPStatus + + +sys.dont_write_bytecode = True # Don't write .pyc files + + +class MothServer(socketserver.ForkingMixIn, http.server.HTTPServer): + def __init__(self, server_address, RequestHandlerClass): + super().__init__(server_address, RequestHandlerClass) + self.args = {} + + +class MothRequestHandler(http.server.SimpleHTTPRequestHandler): + endpoints = [] + + def __init__(self, request, client_address, server): + self.directory = str(server.args["theme_dir"]) + try: + super().__init__(request, client_address, server, directory=server.args["theme_dir"]) + except TypeError: + super().__init__(request, client_address, server) + + + # Backport from Python 3.7 + def translate_path(self, path): + # I guess we just hope that some other thread doesn't call getcwd + getcwd = os.getcwd + os.getcwd = lambda: self.directory + ret = super().translate_path(path) + os.getcwd = getcwd + return ret + + + def get_puzzle(self): + category = self.req.get("cat") + points = int(self.req.get("points")) + catpath = str(self.server.args["puzzles_dir"].joinpath(category)) + cat = moth.Category(catpath, self.seed) + puzzle = cat.puzzle(points) + return puzzle + + + def send_json(self, obj): + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(obj).encode("utf-8")) + + + def handle_register(self): + # Everybody eats when they come to my house + ret = { + "status": "success", + "data": { + "short": "You win", + "description": "Welcome to the development server, you wily hacker you" + } + } + self.send_json(ret) + endpoints.append(('/{seed}/register', handle_register)) + + + def handle_answer(self): + for f in ("cat", "points", "answer"): + self.req[f] = self.fields.getfirst(f) + puzzle = self.get_puzzle() + ret = { + "status": "success", + "data": { + "short": "", + "description": "Provided answer was not in list of answers" + }, + } + + if self.req.get("answer") in puzzle.answers: + ret["data"]["description"] = "Answer is correct" + self.send_json(ret) + endpoints.append(('/{seed}/answer', handle_answer)) + + + def puzzlelist(self): + puzzles = {} + for p in self.server.args["puzzles_dir"].glob("*"): + if not p.is_dir() or p.match(".*"): + continue + catName = p.parts[-1] + cat = moth.Category(str(p), self.seed) + puzzles[catName] = [[i, str(i)] for i in cat.pointvals()] + puzzles[catName].append([0, ""]) + if len(puzzles) <= 1: + logging.warning("No directories found matching {}/*".format(self.server.args["puzzles_dir"])) + + return puzzles + + + # XXX: Remove this (redundant) when we've upgraded the bundled theme (probably v3.6 and beyond) + def handle_puzzlelist(self): + self.send_json(self.puzzlelist()) + endpoints.append(('/{seed}/puzzles.json', handle_puzzlelist)) + + + def handle_state(self): + resp = { + "Config": { + "Devel": True, + }, + "Puzzles": self.puzzlelist(), + "Messages": "

[MOTH Development Server] Participant broadcast messages would go here.

", + } + self.send_json(resp) + endpoints.append(('/{seed}/state', handle_state)) + + + def handle_puzzle(self): + puzzle = self.get_puzzle() + + obj = puzzle.package() + obj["answers"] = puzzle.answers + obj["hint"] = puzzle.hint + obj["summary"] = puzzle.summary + obj["logs"] = puzzle.logs + + self.send_json(obj) + endpoints.append(('/{seed}/content/{cat}/{points}/puzzle.json', handle_puzzle)) + + + def handle_puzzlefile(self): + puzzle = self.get_puzzle() + + try: + file = puzzle.files[self.req["filename"]] + except KeyError: + self.send_error( + HTTPStatus.NOT_FOUND, + "File Not Found: %s" % self.req["filename"], + ) + return + + self.send_response(200) + self.send_header("Content-Type", mimetypes.guess_type(file.name)) + self.end_headers() + shutil.copyfileobj(file.stream, self.wfile) + endpoints.append(("/{seed}/content/{cat}/{points}/{filename}", handle_puzzlefile)) + + + def handle_mothballer(self): + category = self.req.get("cat") + + try: + catdir = self.server.args["puzzles_dir"].joinpath(category) + mb = mothballer.package(category, str(catdir), self.seed) + except Exception as ex: + logging.exception(ex) + self.send_response(500) + self.send_header("Content-Type", "text/html; charset=\"utf-8\"") + self.end_headers() + self.wfile.write(cgitb.html(sys.exc_info()).encode("utf-8")) + return + + self.send_response(200) + self.send_header("Content-Type", "application/octet_stream") + self.end_headers() + shutil.copyfileobj(mb, self.wfile) + endpoints.append(("/{seed}/mothballer/{cat}.mb", handle_mothballer)) + + + def handle_index(self): + seed = random.getrandbits(32) + self.send_response(307) + self.send_header("Location", "%s/" % seed) + self.send_header("Content-Type", "text/html") + self.end_headers() + self.wfile.write(b"Your browser was supposed to redirect you to here." % seed) + endpoints.append((r"/", handle_index)) + + + def handle_theme_file(self): + self.path = "/" + self.req.get("path", "") + super().do_GET() + endpoints.append(("/{seed}/", handle_theme_file)) + endpoints.append(("/{seed}/{path}", handle_theme_file)) + + + def do_GET(self): + self.fields = cgi.FieldStorage( + fp=self.rfile, + headers=self.headers, + environ={ + "REQUEST_METHOD": self.command, + "CONTENT_TYPE": self.headers["Content-Type"], + }, + ) + + url = urllib.parse.urlparse(self.path) + for pattern, function in self.endpoints: + result = parse.parse(pattern, url.path) + if result: + self.req = result.named + seed = self.req.get("seed", "random") + if seed == "random": + self.seed = random.getrandbits(32) + else: + self.seed = int(seed) + return function(self) + super().do_GET() + + def do_POST(self): + self.do_GET() + + def do_HEAD(self): + self.send_error( + HTTPStatus.NOT_IMPLEMENTED, + "Unsupported method (%r)" % self.command, + ) + +# I don't fully understand why you can't do this inside the class definition. +MothRequestHandler.extensions_map[".mjs"] = "application/ecmascript" + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser(description="MOTH puzzle development server") + parser.add_argument( + '--puzzles', default='puzzles', + help="Directory containing your puzzles" + ) + parser.add_argument( + '--theme', default='theme', + help="Directory containing theme files") + parser.add_argument( + '--bind', default="127.0.0.1:8080", + help="Bind to ip:port" + ) + parser.add_argument( + '--base', default="", + help="Base URL to this server, for reverse proxy setup" + ) + parser.add_argument( + "-v", "--verbose", + action="count", + default=1, # Leave at 1, for now, to maintain current default behavior + help="Include more verbose logging. Use multiple flags to increase level", + ) + args = parser.parse_args() + parts = args.bind.split(":") + addr = parts[0] or "0.0.0.0" + port = int(parts[1]) + if args.verbose >= 2: + log_level = logging.DEBUG + elif args.verbose == 1: + log_level = logging.INFO + else: + log_level = logging.WARNING + + logging.basicConfig(level=log_level) + + server = MothServer((addr, port), MothRequestHandler) + server.args["base_url"] = args.base + server.args["puzzles_dir"] = pathlib.Path(args.puzzles) + server.args["theme_dir"] = args.theme + + logging.info("Listening on %s:%d", addr, port) + server.serve_forever() diff --git a/devel/mistune.py b/devel/mistune.py new file mode 100644 index 0000000..a81c4c1 --- /dev/null +++ b/devel/mistune.py @@ -0,0 +1,1190 @@ +# coding: utf-8 +"""mistune + ~~~~~~~ + + The fastest markdown parser in pure Python with renderer feature. + + Copyright (c) 2014 - 2015, Hsiaoming Yang + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the creator nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. +""" + +import re +import inspect + +__version__ = '0.7.3' +__author__ = 'Hsiaoming Yang ' +__all__ = [ + 'BlockGrammar', 'BlockLexer', + 'InlineGrammar', 'InlineLexer', + 'Renderer', 'Markdown', + 'markdown', 'escape', +] + + +_key_pattern = re.compile(r'\s+') +_nonalpha_pattern = re.compile(r'\W') +_escape_pattern = re.compile(r'&(?!#?\w+;)') +_newline_pattern = re.compile(r'\r\n|\r') +_block_quote_leading_pattern = re.compile(r'^ *> ?', flags=re.M) +_block_code_leading_pattern = re.compile(r'^ {4}', re.M) +_inline_tags = [ + 'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data', + 'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u', 'mark', + 'ruby', 'rt', 'rp', 'bdi', 'bdo', 'span', 'br', 'wbr', 'ins', 'del', + 'img', 'font', +] +_pre_tags = ['pre', 'script', 'style'] +_valid_end = r'(?!:/|[^\w\s@]*@)\b' +_valid_attr = r'''\s*[a-zA-Z\-](?:\=(?:"[^"]*"|'[^']*'|\d+))*''' +_block_tag = r'(?!(?:%s)\b)\w+%s' % ('|'.join(_inline_tags), _valid_end) +_scheme_blacklist = ('javascript:', 'vbscript:') + + +def _pure_pattern(regex): + pattern = regex.pattern + if pattern.startswith('^'): + pattern = pattern[1:] + return pattern + + +def _keyify(key): + return _key_pattern.sub(' ', key.lower()) + + +def escape(text, quote=False, smart_amp=True): + """Replace special characters "&", "<" and ">" to HTML-safe sequences. + + The original cgi.escape will always escape "&", but you can control + this one for a smart escape amp. + + :param quote: if set to True, " and ' will be escaped. + :param smart_amp: if set to False, & will always be escaped. + """ + if smart_amp: + text = _escape_pattern.sub('&', text) + else: + text = text.replace('&', '&') + text = text.replace('<', '<') + text = text.replace('>', '>') + if quote: + text = text.replace('"', '"') + text = text.replace("'", ''') + return text + + +def escape_link(url): + """Remove dangerous URL schemes like javascript: and escape afterwards.""" + lower_url = url.lower().strip('\x00\x1a \n\r\t') + for scheme in _scheme_blacklist: + if lower_url.startswith(scheme): + return '' + return escape(url, quote=True, smart_amp=False) + + +def preprocessing(text, tab=4): + text = _newline_pattern.sub('\n', text) + text = text.expandtabs(tab) + text = text.replace('\u00a0', ' ') + text = text.replace('\u2424', '\n') + pattern = re.compile(r'^ +$', re.M) + return pattern.sub('', text) + + +class BlockGrammar(object): + """Grammars for block level tokens.""" + + def_links = re.compile( + r'^ *\[([^^\]]+)\]: *' # [key]: + r']+)>?' # or link + r'(?: +["(]([^\n]+)[")])? *(?:\n+|$)' + ) + def_footnotes = re.compile( + r'^\[\^([^\]]+)\]: *(' + r'[^\n]*(?:\n+|$)' # [^key]: + r'(?: {1,}[^\n]*(?:\n+|$))*' + r')' + ) + + newline = re.compile(r'^\n+') + block_code = re.compile(r'^( {4}[^\n]+\n*)+') + fences = re.compile( + r'^ *(`{3,}|~{3,}) *(\S+)? *\n' # ```lang + r'([\s\S]+?)\s*' + r'\1 *(?:\n+|$)' # ``` + ) + hrule = re.compile(r'^ {0,3}[-*_](?: *[-*_]){2,} *(?:\n+|$)') + heading = re.compile(r'^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)') + lheading = re.compile(r'^([^\n]+)\n *(=|-)+ *(?:\n+|$)') + block_quote = re.compile(r'^( *>[^\n]+(\n[^\n]+)*\n*)+') + list_block = re.compile( + r'^( *)([*+-]|\d+\.) [\s\S]+?' + r'(?:' + r'\n+(?=\1?(?:[-*_] *){3,}(?:\n+|$))' # hrule + r'|\n+(?=%s)' # def links + r'|\n+(?=%s)' # def footnotes + r'|\n{2,}' + r'(?! )' + r'(?!\1(?:[*+-]|\d+\.) )\n*' + r'|' + r'\s*$)' % ( + _pure_pattern(def_links), + _pure_pattern(def_footnotes), + ) + ) + list_item = re.compile( + r'^(( *)(?:[*+-]|\d+\.) [^\n]*' + r'(?:\n(?!\2(?:[*+-]|\d+\.) )[^\n]*)*)', + flags=re.M + ) + list_bullet = re.compile(r'^ *(?:[*+-]|\d+\.) +') + paragraph = re.compile( + r'^((?:[^\n]+\n?(?!' + r'%s|%s|%s|%s|%s|%s|%s|%s|%s' + r'))+)\n*' % ( + _pure_pattern(fences).replace(r'\1', r'\2'), + _pure_pattern(list_block).replace(r'\1', r'\3'), + _pure_pattern(hrule), + _pure_pattern(heading), + _pure_pattern(lheading), + _pure_pattern(block_quote), + _pure_pattern(def_links), + _pure_pattern(def_footnotes), + '<' + _block_tag, + ) + ) + block_html = re.compile( + r'^ *(?:%s|%s|%s) *(?:\n{2,}|\s*$)' % ( + r'', + r'<(%s)((?:%s)*?)>([\s\S]*?)<\/\1>' % (_block_tag, _valid_attr), + r'<%s(?:%s)*?\s*\/?>' % (_block_tag, _valid_attr), + ) + ) + table = re.compile( + r'^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*' + ) + nptable = re.compile( + r'^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*' + ) + text = re.compile(r'^[^\n]+') + + +class BlockLexer(object): + """Block level lexer for block grammars.""" + grammar_class = BlockGrammar + + default_rules = [ + 'newline', 'hrule', 'block_code', 'fences', 'heading', + 'nptable', 'lheading', 'block_quote', + 'list_block', 'block_html', 'def_links', + 'def_footnotes', 'table', 'paragraph', 'text' + ] + + list_rules = ( + 'newline', 'block_code', 'fences', 'lheading', 'hrule', + 'block_quote', 'list_block', 'block_html', 'text', + ) + + footnote_rules = ( + 'newline', 'block_code', 'fences', 'heading', + 'nptable', 'lheading', 'hrule', 'block_quote', + 'list_block', 'block_html', 'table', 'paragraph', 'text' + ) + + def __init__(self, rules=None, **kwargs): + self.tokens = [] + self.def_links = {} + self.def_footnotes = {} + + if not rules: + rules = self.grammar_class() + + self.rules = rules + + def __call__(self, text, rules=None): + return self.parse(text, rules) + + def parse(self, text, rules=None): + text = text.rstrip('\n') + + if not rules: + rules = self.default_rules + + def manipulate(text): + for key in rules: + rule = getattr(self.rules, key) + m = rule.match(text) + if not m: + continue + getattr(self, 'parse_%s' % key)(m) + return m + return False # pragma: no cover + + while text: + m = manipulate(text) + if m is not False: + text = text[len(m.group(0)):] + continue + if text: # pragma: no cover + raise RuntimeError('Infinite loop at: %s' % text) + return self.tokens + + def parse_newline(self, m): + length = len(m.group(0)) + if length > 1: + self.tokens.append({'type': 'newline'}) + + def parse_block_code(self, m): + # clean leading whitespace + code = _block_code_leading_pattern.sub('', m.group(0)) + self.tokens.append({ + 'type': 'code', + 'lang': None, + 'text': code, + }) + + def parse_fences(self, m): + self.tokens.append({ + 'type': 'code', + 'lang': m.group(2), + 'text': m.group(3), + }) + + def parse_heading(self, m): + self.tokens.append({ + 'type': 'heading', + 'level': len(m.group(1)), + 'text': m.group(2), + }) + + def parse_lheading(self, m): + """Parse setext heading.""" + self.tokens.append({ + 'type': 'heading', + 'level': 1 if m.group(2) == '=' else 2, + 'text': m.group(1), + }) + + def parse_hrule(self, m): + self.tokens.append({'type': 'hrule'}) + + def parse_list_block(self, m): + bull = m.group(2) + self.tokens.append({ + 'type': 'list_start', + 'ordered': '.' in bull, + }) + cap = m.group(0) + self._process_list_item(cap, bull) + self.tokens.append({'type': 'list_end'}) + + def _process_list_item(self, cap, bull): + cap = self.rules.list_item.findall(cap) + + _next = False + length = len(cap) + + for i in range(length): + item = cap[i][0] + + # remove the bullet + space = len(item) + item = self.rules.list_bullet.sub('', item) + + # outdent + if '\n ' in item: + space = space - len(item) + pattern = re.compile(r'^ {1,%d}' % space, flags=re.M) + item = pattern.sub('', item) + + # determine whether item is loose or not + loose = _next + if not loose and re.search(r'\n\n(?!\s*$)', item): + loose = True + + rest = len(item) + if i != length - 1 and rest: + _next = item[rest - 1] == '\n' + if not loose: + loose = _next + + if loose: + t = 'loose_item_start' + else: + t = 'list_item_start' + + self.tokens.append({'type': t}) + # recurse + self.parse(item, self.list_rules) + self.tokens.append({'type': 'list_item_end'}) + + def parse_block_quote(self, m): + self.tokens.append({'type': 'block_quote_start'}) + # clean leading > + cap = _block_quote_leading_pattern.sub('', m.group(0)) + self.parse(cap) + self.tokens.append({'type': 'block_quote_end'}) + + def parse_def_links(self, m): + key = _keyify(m.group(1)) + self.def_links[key] = { + 'link': m.group(2), + 'title': m.group(3), + } + + def parse_def_footnotes(self, m): + key = _keyify(m.group(1)) + if key in self.def_footnotes: + # footnote is already defined + return + + self.def_footnotes[key] = 0 + + self.tokens.append({ + 'type': 'footnote_start', + 'key': key, + }) + + text = m.group(2) + + if '\n' in text: + lines = text.split('\n') + whitespace = None + for line in lines[1:]: + space = len(line) - len(line.lstrip()) + if space and (not whitespace or space < whitespace): + whitespace = space + newlines = [lines[0]] + for line in lines[1:]: + newlines.append(line[whitespace:]) + text = '\n'.join(newlines) + + self.parse(text, self.footnote_rules) + + self.tokens.append({ + 'type': 'footnote_end', + 'key': key, + }) + + def parse_table(self, m): + item = self._process_table(m) + + cells = re.sub(r'(?: *\| *)?\n$', '', m.group(3)) + cells = cells.split('\n') + for i, v in enumerate(cells): + v = re.sub(r'^ *\| *| *\| *$', '', v) + cells[i] = re.split(r' *\| *', v) + + item['cells'] = cells + self.tokens.append(item) + + def parse_nptable(self, m): + item = self._process_table(m) + + cells = re.sub(r'\n$', '', m.group(3)) + cells = cells.split('\n') + for i, v in enumerate(cells): + cells[i] = re.split(r' *\| *', v) + + item['cells'] = cells + self.tokens.append(item) + + def _process_table(self, m): + header = re.sub(r'^ *| *\| *$', '', m.group(1)) + header = re.split(r' *\| *', header) + align = re.sub(r' *|\| *$', '', m.group(2)) + align = re.split(r' *\| *', align) + + for i, v in enumerate(align): + if re.search(r'^ *-+: *$', v): + align[i] = 'right' + elif re.search(r'^ *:-+: *$', v): + align[i] = 'center' + elif re.search(r'^ *:-+ *$', v): + align[i] = 'left' + else: + align[i] = None + + item = { + 'type': 'table', + 'header': header, + 'align': align, + } + return item + + def parse_block_html(self, m): + tag = m.group(1) + if not tag: + text = m.group(0) + self.tokens.append({ + 'type': 'close_html', + 'text': text + }) + else: + attr = m.group(2) + text = m.group(3) + self.tokens.append({ + 'type': 'open_html', + 'tag': tag, + 'extra': attr, + 'text': text + }) + + def parse_paragraph(self, m): + text = m.group(1).rstrip('\n') + self.tokens.append({'type': 'paragraph', 'text': text}) + + def parse_text(self, m): + text = m.group(0) + self.tokens.append({'type': 'text', 'text': text}) + + +class InlineGrammar(object): + """Grammars for inline level tokens.""" + + escape = re.compile(r'^\\([\\`*{}\[\]()#+\-.!_>~|])') # \* \+ \! .... + inline_html = re.compile( + r'^(?:%s|%s|%s)' % ( + r'', + r'<(\w+%s)((?:%s)*?)\s*>([\s\S]*?)<\/\1>' % (_valid_end, _valid_attr), + r'<\w+%s(?:%s)*?\s*\/?>' % (_valid_end, _valid_attr), + ) + ) + autolink = re.compile(r'^<([^ >]+(@|:)[^ >]+)>') + link = re.compile( + r'^!?\[(' + r'(?:\[[^^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*' + r')\]\(' + r'''\s*(<)?([\s\S]*?)(?(2)>)(?:\s+['"]([\s\S]*?)['"])?\s*''' + r'\)' + ) + reflink = re.compile( + r'^!?\[(' + r'(?:\[[^^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*' + r')\]\s*\[([^^\]]*)\]' + ) + nolink = re.compile(r'^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]') + url = re.compile(r'''^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])''') + double_emphasis = re.compile( + r'^_{2}([\s\S]+?)_{2}(?!_)' # __word__ + r'|' + r'^\*{2}([\s\S]+?)\*{2}(?!\*)' # **word** + ) + emphasis = re.compile( + r'^\b_((?:__|[^_])+?)_\b' # _word_ + r'|' + r'^\*((?:\*\*|[^\*])+?)\*(?!\*)' # *word* + ) + code = re.compile(r'^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)') # `code` + linebreak = re.compile(r'^ {2,}\n(?!\s*$)') + strikethrough = re.compile(r'^~~(?=\S)([\s\S]*?\S)~~') # ~~word~~ + footnote = re.compile(r'^\[\^([^\]]+)\]') + text = re.compile(r'^[\s\S]+?(?=[\\%s' % (tag, extra, text, tag) + else: + html = m.group(0) + return self.renderer.inline_html(html) + + def output_footnote(self, m): + key = _keyify(m.group(1)) + if key not in self.footnotes: + return None + if self.footnotes[key]: + return None + self.footnote_index += 1 + self.footnotes[key] = self.footnote_index + return self.renderer.footnote_ref(key, self.footnote_index) + + def output_link(self, m): + return self._process_link(m, m.group(3), m.group(4)) + + def output_reflink(self, m): + key = _keyify(m.group(2) or m.group(1)) + if key not in self.links: + return None + ret = self.links[key] + return self._process_link(m, ret['link'], ret['title']) + + def output_nolink(self, m): + key = _keyify(m.group(1)) + if key not in self.links: + return None + ret = self.links[key] + return self._process_link(m, ret['link'], ret['title']) + + def _process_link(self, m, link, title=None): + line = m.group(0) + text = m.group(1) + if line[0] == '!': + return self.renderer.image(link, title, text) + + self._in_link = True + text = self.output(text) + self._in_link = False + return self.renderer.link(link, title, text) + + def output_double_emphasis(self, m): + text = m.group(2) or m.group(1) + text = self.output(text) + return self.renderer.double_emphasis(text) + + def output_emphasis(self, m): + text = m.group(2) or m.group(1) + text = self.output(text) + return self.renderer.emphasis(text) + + def output_code(self, m): + text = m.group(2) + return self.renderer.codespan(text) + + def output_linebreak(self, m): + return self.renderer.linebreak() + + def output_strikethrough(self, m): + text = self.output(m.group(1)) + return self.renderer.strikethrough(text) + + def output_text(self, m): + text = m.group(0) + return self.renderer.text(text) + + +class Renderer(object): + """The default HTML renderer for rendering Markdown. + """ + + def __init__(self, **kwargs): + self.options = kwargs + + def placeholder(self): + """Returns the default, empty output value for the renderer. + + All renderer methods use the '+=' operator to append to this value. + Default is a string so rendering HTML can build up a result string with + the rendered Markdown. + + Can be overridden by Renderer subclasses to be types like an empty + list, allowing the renderer to create a tree-like structure to + represent the document (which can then be reprocessed later into a + separate format like docx or pdf). + """ + return '' + + def block_code(self, code, lang=None): + """Rendering block level code. ``pre > code``. + + :param code: text content of the code block. + :param lang: language of the given code. + """ + code = code.rstrip('\n') + if not lang: + code = escape(code, smart_amp=False) + return '
%s\n
\n' % code + code = escape(code, quote=True, smart_amp=False) + return '
%s\n
\n' % (lang, code) + + def block_quote(self, text): + """Rendering
with the given text. + + :param text: text content of the blockquote. + """ + return '
%s\n
\n' % text.rstrip('\n') + + def block_html(self, html): + """Rendering block level pure html content. + + :param html: text content of the html snippet. + """ + if self.options.get('skip_style') and \ + html.lower().startswith('`` ``

``. + + :param text: rendered text content for the header. + :param level: a number for the header level, for example: 1. + :param raw: raw text content of the header. + """ + return '%s\n' % (level, text, level) + + def hrule(self): + """Rendering method for ``
`` tag.""" + if self.options.get('use_xhtml'): + return '
\n' + return '
\n' + + def list(self, body, ordered=True): + """Rendering list tags like ``
    `` and ``
      ``. + + :param body: body contents of the list. + :param ordered: whether this list is ordered or not. + """ + tag = 'ul' + if ordered: + tag = 'ol' + return '<%s>\n%s\n' % (tag, body, tag) + + def list_item(self, text): + """Rendering list item snippet. Like ``
    1. ``.""" + return '
    2. %s
    3. \n' % text + + def paragraph(self, text): + """Rendering paragraph tags. Like ``

      ``.""" + return '

      %s

      \n' % text.strip(' ') + + def table(self, header, body): + """Rendering table element. Wrap header and body in it. + + :param header: header part of the table. + :param body: body part of the table. + """ + return ( + '\n%s\n' + '\n%s\n
      \n' + ) % (header, body) + + def table_row(self, content): + """Rendering a table row. Like ````. + + :param content: content of current table row. + """ + return '\n%s\n' % content + + def table_cell(self, content, **flags): + """Rendering a table cell. Like ```` ````. + + :param content: content of current table cell. + :param header: whether this is header or not. + :param align: align of current table cell. + """ + if flags['header']: + tag = 'th' + else: + tag = 'td' + align = flags['align'] + if not align: + return '<%s>%s\n' % (tag, content, tag) + return '<%s style="text-align:%s">%s\n' % ( + tag, align, content, tag + ) + + def double_emphasis(self, text): + """Rendering **strong** text. + + :param text: text content for emphasis. + """ + return '%s' % text + + def emphasis(self, text): + """Rendering *emphasis* text. + + :param text: text content for emphasis. + """ + return '%s' % text + + def codespan(self, text): + """Rendering inline `code` text. + + :param text: text content for inline code. + """ + text = escape(text.rstrip(), smart_amp=False) + return '%s' % text + + def linebreak(self): + """Rendering line break like ``
      ``.""" + if self.options.get('use_xhtml'): + return '
      \n' + return '
      \n' + + def strikethrough(self, text): + """Rendering ~~strikethrough~~ text. + + :param text: text content for strikethrough. + """ + return '%s' % text + + def text(self, text): + """Rendering unformatted text. + + :param text: text content. + """ + return escape(text) + + def escape(self, text): + """Rendering escape sequence. + + :param text: text content. + """ + return escape(text) + + def autolink(self, link, is_email=False): + """Rendering a given link or email address. + + :param link: link content or email address. + :param is_email: whether this is an email or not. + """ + text = link = escape(link) + if is_email: + link = 'mailto:%s' % link + return '%s' % (link, text) + + def link(self, link, title, text): + """Rendering a given link with content and title. + + :param link: href link for ```` tag. + :param title: title content for `title` attribute. + :param text: text content for description. + """ + link = escape_link(link) + if not title: + return '%s' % (link, text) + title = escape(title, quote=True) + return '%s' % (link, title, text) + + def image(self, src, title, text): + """Rendering a image with title and text. + + :param src: source link of the image. + :param title: title text of the image. + :param text: alt text of the image. + """ + src = escape_link(src) + text = escape(text, quote=True) + if title: + title = escape(title, quote=True) + html = '%s' % html + return '%s>' % html + + def inline_html(self, html): + """Rendering span level pure html content. + + :param html: text content of the html snippet. + """ + if self.options.get('escape'): + return escape(html) + return html + + def newline(self): + """Rendering newline element.""" + return '' + + def footnote_ref(self, key, index): + """Rendering the ref anchor of a footnote. + + :param key: identity key for the footnote. + :param index: the index count of current footnote. + """ + html = ( + '' + '%d' + ) % (escape(key), escape(key), index) + return html + + def footnote_item(self, key, text): + """Rendering a footnote item. + + :param key: identity key for the footnote. + :param text: text content of the footnote. + """ + back = ( + '' + ) % escape(key) + text = text.rstrip() + if text.endswith('

      '): + text = re.sub(r'<\/p>$', r'%s

      ' % back, text) + else: + text = '%s

      %s

      ' % (text, back) + html = '
    4. %s
    5. \n' % (escape(key), text) + return html + + def footnotes(self, text): + """Wrapper for all footnotes. + + :param text: contents of all footnotes. + """ + html = '
      \n%s
        %s
      \n
      \n' + return html % (self.hrule(), text) + + +class Markdown(object): + """The Markdown parser. + + :param renderer: An instance of ``Renderer``. + :param inline: An inline lexer class or instance. + :param block: A block lexer class or instance. + """ + def __init__(self, renderer=None, inline=None, block=None, **kwargs): + if not renderer: + renderer = Renderer(**kwargs) + else: + kwargs.update(renderer.options) + + self.renderer = renderer + + if inline and inspect.isclass(inline): + inline = inline(renderer, **kwargs) + if block and inspect.isclass(block): + block = block(**kwargs) + + if inline: + self.inline = inline + else: + self.inline = InlineLexer(renderer, **kwargs) + + self.block = block or BlockLexer(BlockGrammar()) + self.footnotes = [] + self.tokens = [] + + # detect if it should parse text in block html + self._parse_block_html = kwargs.get('parse_block_html') + + def __call__(self, text): + return self.parse(text) + + def render(self, text): + """Render the Markdown text. + + :param text: markdown formatted text content. + """ + return self.parse(text) + + def parse(self, text): + out = self.output(preprocessing(text)) + + keys = self.block.def_footnotes + + # reset block + self.block.def_links = {} + self.block.def_footnotes = {} + + # reset inline + self.inline.links = {} + self.inline.footnotes = {} + + if not self.footnotes: + return out + + footnotes = filter(lambda o: keys.get(o['key']), self.footnotes) + self.footnotes = sorted( + footnotes, key=lambda o: keys.get(o['key']), reverse=True + ) + + body = self.renderer.placeholder() + while self.footnotes: + note = self.footnotes.pop() + body += self.renderer.footnote_item( + note['key'], note['text'] + ) + + out += self.renderer.footnotes(body) + return out + + def pop(self): + if not self.tokens: + return None + self.token = self.tokens.pop() + return self.token + + def peek(self): + if self.tokens: + return self.tokens[-1] + return None # pragma: no cover + + def output(self, text, rules=None): + self.tokens = self.block(text, rules) + self.tokens.reverse() + + self.inline.setup(self.block.def_links, self.block.def_footnotes) + + out = self.renderer.placeholder() + while self.pop(): + out += self.tok() + return out + + def tok(self): + t = self.token['type'] + + # sepcial cases + if t.endswith('_start'): + t = t[:-6] + + return getattr(self, 'output_%s' % t)() + + def tok_text(self): + text = self.token['text'] + while self.peek()['type'] == 'text': + text += '\n' + self.pop()['text'] + return self.inline(text) + + def output_newline(self): + return self.renderer.newline() + + def output_hrule(self): + return self.renderer.hrule() + + def output_heading(self): + return self.renderer.header( + self.inline(self.token['text']), + self.token['level'], + self.token['text'], + ) + + def output_code(self): + return self.renderer.block_code( + self.token['text'], self.token['lang'] + ) + + def output_table(self): + aligns = self.token['align'] + aligns_length = len(aligns) + cell = self.renderer.placeholder() + + # header part + header = self.renderer.placeholder() + for i, value in enumerate(self.token['header']): + align = aligns[i] if i < aligns_length else None + flags = {'header': True, 'align': align} + cell += self.renderer.table_cell(self.inline(value), **flags) + + header += self.renderer.table_row(cell) + + # body part + body = self.renderer.placeholder() + for i, row in enumerate(self.token['cells']): + cell = self.renderer.placeholder() + for j, value in enumerate(row): + align = aligns[j] if j < aligns_length else None + flags = {'header': False, 'align': align} + cell += self.renderer.table_cell(self.inline(value), **flags) + body += self.renderer.table_row(cell) + + return self.renderer.table(header, body) + + def output_block_quote(self): + body = self.renderer.placeholder() + while self.pop()['type'] != 'block_quote_end': + body += self.tok() + return self.renderer.block_quote(body) + + def output_list(self): + ordered = self.token['ordered'] + body = self.renderer.placeholder() + while self.pop()['type'] != 'list_end': + body += self.tok() + return self.renderer.list(body, ordered) + + def output_list_item(self): + body = self.renderer.placeholder() + while self.pop()['type'] != 'list_item_end': + if self.token['type'] == 'text': + body += self.tok_text() + else: + body += self.tok() + + return self.renderer.list_item(body) + + def output_loose_item(self): + body = self.renderer.placeholder() + while self.pop()['type'] != 'list_item_end': + body += self.tok() + return self.renderer.list_item(body) + + def output_footnote(self): + self.inline._in_footnote = True + body = self.renderer.placeholder() + key = self.token['key'] + while self.pop()['type'] != 'footnote_end': + body += self.tok() + self.footnotes.append({'key': key, 'text': body}) + self.inline._in_footnote = False + return self.renderer.placeholder() + + def output_close_html(self): + text = self.token['text'] + return self.renderer.block_html(text) + + def output_open_html(self): + text = self.token['text'] + tag = self.token['tag'] + if self._parse_block_html and tag not in _pre_tags: + text = self.inline(text, rules=self.inline.inline_html_rules) + extra = self.token.get('extra') or '' + html = '<%s%s>%s' % (tag, extra, text, tag) + return self.renderer.block_html(html) + + def output_paragraph(self): + return self.renderer.paragraph(self.inline(self.token['text'])) + + def output_text(self): + return self.renderer.paragraph(self.tok_text()) + + +def markdown(text, escape=True, **kwargs): + """Render markdown formatted text to html. + + :param text: markdown formatted text content. + :param escape: if set to False, all html tags will not be escaped. + :param use_xhtml: output with xhtml tags. + :param hard_wrap: if set to True, it will use the GFM line breaks feature. + :param parse_block_html: parse text only in block level html. + :param parse_inline_html: parse text only in inline level html. + """ + return Markdown(escape=escape, **kwargs)(text) diff --git a/devel/moth.py b/devel/moth.py new file mode 100644 index 0000000..c6699fa --- /dev/null +++ b/devel/moth.py @@ -0,0 +1,469 @@ +#!/usr/bin/python3 + +import argparse +import contextlib +import copy +import glob +import hashlib +import html +import io +import importlib.machinery +import logging +import mistune +import os +import random +import string +import sys +import tempfile +import shlex +import yaml + +messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + +LOGGER = logging.getLogger(__name__) + +def djb2hash(str): + h = 5381 + for c in str.encode("utf-8"): + h = ((h * 33) + c) & 0xffffffff + return h + +@contextlib.contextmanager +def pushd(newdir): + curdir = os.getcwd() + LOGGER.debug("Attempting to chdir from %s to %s" % (curdir, newdir)) + os.chdir(newdir) + + # Force a copy of the old path, instead of just a reference + old_path = list(sys.path) + old_modules = copy.copy(sys.modules) + sys.path.append(newdir) + + try: + yield + finally: + # Restore the old path + to_remove = [] + for module in sys.modules: + if module not in old_modules: + to_remove.append(module) + + for module in to_remove: + del(sys.modules[module]) + + sys.path = old_path + LOGGER.debug("Changing directory back from %s to %s" % (newdir, curdir)) + os.chdir(curdir) + + +def loadmod(name, path): + abspath = os.path.abspath(path) + loader = importlib.machinery.SourceFileLoader(name, abspath) + return loader.load_module() + + +# Get a big list of clean words for our answer file. +ANSWER_WORDS = [w.strip() for w in open(os.path.join(os.path.dirname(__file__), + 'answer_words.txt'))] + +class PuzzleFile: + """A file associated with a puzzle. + + path: The path to the original input file. May be None (when this is created from a file handle + and there is no original input. + handle: A File-like object set to read the file from. You should be able to read straight + from it without having to seek to the beginning of the file. + name: The name of the output file. + visible: A boolean indicating whether this file should visible to the user. If False, + the file is still expected to be accessible, but it's path must be known + (or figured out) to retrieve it.""" + + def __init__(self, stream, name, visible=True): + self.stream = stream + self.name = name + self.visible = visible + +class PuzzleSuccess(dict): + """Puzzle success objectives + + :param acceptable: Learning outcome from acceptable knowledge of the subject matter + :param mastery: Learning outcome from mastery of the subject matter + """ + + valid_fields = ["acceptable", "mastery"] + + def __init__(self, **kwargs): + super(PuzzleSuccess, self).__init__() + for key in self.valid_fields: + self[key] = None + for key, value in kwargs.items(): + if key in self.valid_fields: + self[key] = value + + def __getattr__(self, attr): + if attr in self.valid_fields: + return self[attr] + raise AttributeError("'%s' object has no attribute '%s'" % (type(self).__name__, attr)) + + def __setattr__(self, attr, value): + if attr in self.valid_fields: + self[attr] = value + else: + raise AttributeError("'%s' object has no attribute '%s'" % (type(self).__name__, attr)) + + +class Puzzle: + def __init__(self, category_seed, points): + """A MOTH Puzzle. + + :param category_seed: A byte string to use as a seed for random numbers for this puzzle. + It is combined with the puzzle points. + :param points: The point value of the puzzle. + """ + + super().__init__() + + self.points = points + self.summary = None + self.authors = [] + self.answers = [] + self.scripts = [] + self.pattern = None + self.hint = None + self.files = {} + self.body = io.StringIO() + + # NIST NICE objective content + self.objective = None # Text describing the expected learning outcome from solving this puzzle, *why* are you solving this puzzle + self.success = PuzzleSuccess() # Text describing criteria for different levels of success, e.g. {"Acceptable": "Did OK", "Mastery": "Did even better"} + self.solution = None # Text describing how to solve the puzzle + self.ksas = [] # A list of references to related NICE KSAs (e.g. K0058, . . .) + + self.logs = [] + self.randseed = category_seed * self.points + self.rand = random.Random(self.randseed) + + def log(self, *vals): + """Add a new log message to this puzzle.""" + msg = ' '.join(str(v) for v in vals) + self.logs.append(msg) + + def read_stream(self, stream): + header = True + line = "" + if stream.read(3) == "---": + header = "yaml" + else: + header = "moth" + + stream.seek(0) + + if header == "yaml": + LOGGER.info("Puzzle is YAML-formatted") + self.read_yaml_header(stream) + elif header == "moth": + LOGGER.info("Puzzle is MOTH-formatted") + self.read_moth_header(stream) + + for line in stream: + self.body.write(line) + + def read_yaml_header(self, stream): + contents = "" + header = False + for line in stream: + if line.strip() == "---" and header: # Handle last line + break + elif line.strip() == "---": # Handle first line + header = True + continue + else: + contents += line + + config = yaml.safe_load(contents) + for key, value in config.items(): + key = key.lower() + self.handle_header_key(key, value) + + def read_moth_header(self, stream): + for line in stream: + line = line.strip() + if not line: + break + + key, val = line.split(':', 1) + key = key.lower() + val = val.strip() + self.handle_header_key(key, val) + + def handle_header_key(self, key, val): + LOGGER.debug("Handling key: %s, value: %s", key, val) + if key == 'author': + self.authors.append(val) + elif key == 'authors': + if not isinstance(val, list): + raise ValueError("Authors must be a list, got %s, instead" & (type(val),)) + self.authors = list(val) + elif key == 'summary': + self.summary = val + elif key == 'answer': + if not isinstance(val, str): + raise ValueError("Answers must be strings, got %s, instead" % (type(val),)) + self.answers.append(val) + elif key == "answers": + for answer in val: + if not isinstance(answer, str): + raise ValueError("Answers must be strings, got %s, instead" % (type(answer),)) + self.answers.append(answer) + elif key == 'pattern': + self.pattern = val + elif key == 'hint': + self.hint = val + elif key == 'name': + pass + elif key == 'file': + parts = shlex.split(val) + name = parts[0] + hidden = False + LOGGER.debug("Attempting to open %s", os.path.abspath(name)) + stream = open(name, 'rb') + try: + name = parts[1] + hidden = (parts[2].lower() == "hidden") + except IndexError: + pass + self.files[name] = PuzzleFile(stream, name, not hidden) + elif key == 'files': + for file in val: + path = file["path"] + stream = open(path, "rb") + name = file.get("name") or path + self.files[name] = PuzzleFile(stream, name, not file.get("hidden")) + elif key == 'script': + stream = open(val, 'rb') + self.add_script_stream(stream, val) + elif key == "objective": + self.objective = val + elif key == "success": + # Force success dictionary keys to be lower-case + self.success = dict((x.lower(), y) for x,y in val.items()) + elif key == "success.acceptable": + self.success.acceptable = val + elif key == "success.mastery": + self.success.mastery = val + elif key == "solution": + self.solution = val + elif key == "ksas": + if not isinstance(val, list): + raise ValueError("KSAs must be a list, got %s, instead" & (type(val),)) + self.ksas = val + elif key == "ksa": + self.ksas.append(val) + else: + raise ValueError("Unrecognized header field: {}".format(key)) + + + def read_directory(self, path): + try: + puzzle_mod = loadmod("puzzle", os.path.join(path, "puzzle.py")) + except FileNotFoundError: + puzzle_mod = None + + with pushd(path): + if puzzle_mod: + puzzle_mod.make(self) + elif os.path.exists('puzzle.moth'): + with open('puzzle.moth') as f: + self.read_stream(f) + else: + self.authors = ["boggarts"] + self.body.write("This puzzle is broken! It has no puzzle.py or puzzle.moth.") + + def random_hash(self): + """Create a file basename (no extension) with our number generator.""" + return ''.join(self.rand.choice(string.ascii_lowercase) for i in range(8)) + + def make_temp_file(self, name=None, visible=True): + """Get a file object for adding dynamically generated data to the puzzle. When you're + done with this file, flush it, but don't close it. + + :param name: The name of the file for links within the puzzle. If this is None, a name + will be generated for you. + :param visible: Whether or not the file will be visible to the user. + :return: A file object for writing + """ + + stream = tempfile.TemporaryFile() + self.add_stream(stream, name, visible) + return stream + + def add_script_stream(self, stream, name): + # Make sure this shows up in the header block of the HTML output. + self.files[name] = PuzzleFile(stream, name, visible=False) + self.scripts.append(name) + + def add_stream(self, stream, name=None, visible=True): + if name is None: + name = self.random_hash() + self.files[name] = PuzzleFile(stream, name, visible) + + def add_file(self, filename, visible=True): + fd = open(filename, 'rb') + name = os.path.basename(filename) + self.add_stream(fd, name=name, visible=visible) + + def randword(self): + """Return a randomly-chosen word""" + + return self.rand.choice(ANSWER_WORDS) + + def make_answer(self, word_count=4, sep=' '): + """Generate and return a new answer. It's automatically added to the puzzle answer list. + :param int word_count: The number of words to include in the answer. + :param str|bytes sep: The word separator. + :returns: The answer string + """ + + words = [self.randword() for i in range(word_count)] + answer = sep.join(words) + self.answers.append(answer) + return answer + + hexdump_stdch = stdch = ( + 'Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·' + 'Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·' + ' !"#$%&\'()*+,-./' + '0123456789:;<=>?' + '@ABCDEFGHIJKLMNO' + 'PQRSTUVWXYZ[\]^_' + '`abcdefghijklmno' + 'pqrstuvwxyz{|}~Ā·' + 'Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·' + 'Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·' + 'Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·' + 'Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·' + 'Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·' + 'Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·' + 'Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·' + 'Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·Ā·' + ) + + def hexdump(self, buf, charset=hexdump_stdch, gap=('ļæ½', 'āŒ·')): + hexes, chars = [], [] + out = [] + + for b in buf: + if len(chars) == 16: + out.append((hexes, chars)) + hexes, chars = [], [] + + if b is None: + h, c = gap + else: + h = '{:02x}'.format(b) + c = charset[b] + chars.append(c) + hexes.append(h) + + out.append((hexes, chars)) + + offset = 0 + elided = False + lastchars = None + self.body.write('
      ')
      +        for hexes, chars in out:
      +            if chars == lastchars:
      +                offset += len(chars)
      +                if not elided:
      +                    self.body.write('*\n')
      +                    elided = True
      +                continue
      +            lastchars = chars[:]
      +            elided = False
      +
      +            pad = 16 - len(chars)
      +            hexes += ['  '] * pad
      +
      +            self.body.write('{:08x}  '.format(offset))
      +            self.body.write(' '.join(hexes[:8]))
      +            self.body.write('  ')
      +            self.body.write(' '.join(hexes[8:]))
      +            self.body.write('  |')
      +            self.body.write(html.escape(''.join(chars)))
      +            self.body.write('|\n')
      +            offset += len(chars)
      +        self.body.write('{:08x}\n'.format(offset))
      +        self.body.write('
      ') + + def get_authors(self): + return self.authors or [self.author] + + def get_body(self): + return self.body.getvalue() + + def html_body(self): + """Format and return the markdown for the puzzle body.""" + return mistune.markdown(self.get_body(), escape=False) + + def package(self, answers=False): + """Return a dict packaging of the puzzle.""" + + files = [fn for fn,f in self.files.items() if f.visible] + hidden = [fn for fn,f in self.files.items() if not f.visible] + return { + 'authors': self.get_authors(), + 'hashes': self.hashes(), + 'files': files, + 'hidden': hidden, + 'scripts': self.scripts, + 'pattern': self.pattern, + 'body': self.html_body(), + 'objective': self.objective, + 'success': self.success, + 'solution': self.solution, + 'ksas': self.ksas, + } + + def hashes(self): + "Return a list of answer hashes" + + return [djb2hash(a) for a in self.answers] + + +class Category: + def __init__(self, path, seed): + self.path = path + self.seed = seed + self.catmod = None + + try: + self.catmod = loadmod('category', os.path.join(path, 'category.py')) + except FileNotFoundError: + self.catmod = None + + def pointvals(self): + if self.catmod: + with pushd(self.path): + pointvals = self.catmod.pointvals() + else: + pointvals = [] + for fpath in glob.glob(os.path.join(self.path, "[0-9]*")): + pn = os.path.basename(fpath) + points = int(pn) + pointvals.append(points) + return sorted(pointvals) + + def puzzle(self, points): + puzzle = Puzzle(self.seed, points) + path = os.path.join(self.path, str(points)) + if self.catmod: + with pushd(self.path): + self.catmod.make(points, puzzle) + else: + with pushd(self.path): + puzzle.read_directory(path) + return puzzle + + def __iter__(self): + for points in self.pointvals(): + yield self.puzzle(points) diff --git a/devel/mothballer.py b/devel/mothballer.py new file mode 100755 index 0000000..5ec9df6 --- /dev/null +++ b/devel/mothballer.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +import argparse +import binascii +import hashlib +import io +import json +import logging +import moth +import os +import shutil +import tempfile +import zipfile +import random + +SEEDFN = "SEED" + + +def write_kv_pairs(ziphandle, filename, kv): + """ Write out a sorted map to file + :param ziphandle: a zipfile object + :param filename: The filename to write within the zipfile object + :param kv: the map to write out + :return: + """ + filehandle = io.StringIO() + for key in sorted(kv.keys()): + if isinstance(kv[key], list): + for val in kv[key]: + filehandle.write("%s %s\n" % (key, val)) + else: + filehandle.write("%s %s\n" % (key, kv[key])) + filehandle.seek(0) + ziphandle.writestr(filename, filehandle.read()) + + +def escape(s): + return s.replace('&', '&').replace('<', '<').replace('>', '>') + + +def build_category(categorydir, outdir): + category_seed = random.getrandbits(32) + + categoryname = os.path.basename(categorydir.strip(os.sep)) + zipfilename = os.path.join(outdir, "%s.mb" % categoryname) + logging.info("Building {} from {}".format(zipfilename, categorydir)) + + if os.path.exists(zipfilename): + # open and gather some state + existing = zipfile.ZipFile(zipfilename, 'r') + try: + category_seed = int(existing.open(SEEDFN).read().strip()) + except Exception: + pass + existing.close() + logging.debug("Using PRNG seed {}".format(category_seed)) + + zipfileraw = tempfile.NamedTemporaryFile(delete=False) + mothball = package(categoryname, categorydir, category_seed) + shutil.copyfileobj(mothball, zipfileraw) + zipfileraw.close() + shutil.move(zipfileraw.name, zipfilename) + + +# Returns a file-like object containing the contents of the new zip file +def package(categoryname, categorydir, seed): + zfraw = io.BytesIO() + zf = zipfile.ZipFile(zfraw, 'x') + zf.writestr("category_seed.txt", str(seed)) + + cat = moth.Category(categorydir, seed) + answers = {} + summary = {} + for puzzle in cat: + logging.info("Processing point value {}".format(puzzle.points)) + + answers[puzzle.points] = puzzle.answers + summary[puzzle.points] = puzzle.summary + + puzzledir = os.path.join("content", str(puzzle.points)) + for fn, f in puzzle.files.items(): + payload = f.stream.read() + zf.writestr(os.path.join(puzzledir, fn), payload) + + obj = puzzle.package() + zf.writestr(os.path.join(puzzledir, 'puzzle.json'), json.dumps(obj)) + + write_kv_pairs(zf, 'answers.txt', answers) + write_kv_pairs(zf, 'summaries.txt', summary) + + # clean up + zf.close() + zfraw.seek(0) + return zfraw + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Build a category package') + parser.add_argument('outdir', help='Output directory') + parser.add_argument('categorydirs', nargs='+', help='Directory of category source') + args = parser.parse_args() + + logging.basicConfig(level=logging.DEBUG) + + outdir = os.path.abspath(args.outdir) + for categorydir in args.categorydirs: + categorydir = os.path.abspath(categorydir) + build_category(categorydir, outdir) diff --git a/devel/mothd.service b/devel/mothd.service new file mode 100644 index 0000000..664afcb --- /dev/null +++ b/devel/mothd.service @@ -0,0 +1,18 @@ +# To install: +# sudo cp mothd.service /etc/systemd/system/moth.service +# sudo systemctl enable mothd +# sudo systemctl start mothd + +[Unit] +Description=Monarch Of The Hill server +After=network.target auditd.service + +[Service] +WorkingDirectory=/srv/moth +User=www-data +ExecStart=/srv/moth/mothd +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/devel/parse.py b/devel/parse.py new file mode 100644 index 0000000..80ca637 --- /dev/null +++ b/devel/parse.py @@ -0,0 +1,1335 @@ +# -*- encoding: utf-8 -*- +r'''Parse strings using a specification based on the Python format() syntax. + + ``parse()`` is the opposite of ``format()`` + +The module is set up to only export ``parse()``, ``search()``, ``findall()``, +and ``with_pattern()`` when ``import \*`` is used: + +>>> from parse import * + +From there it's a simple thing to parse a string: + +>>> parse("It's {}, I love it!", "It's spam, I love it!") + +>>> _[0] +'spam' + +Or to search a string for some pattern: + +>>> search('Age: {:d}\n', 'Name: Rufus\nAge: 42\nColor: red\n') + + +Or find all the occurrences of some pattern in a string: + +>>> ''.join(r.fixed[0] for r in findall(">{}<", "

      the bold text

      ")) +'the bold text' + +If you're going to use the same pattern to match lots of strings you can +compile it once: + +>>> from parse import compile +>>> p = compile("It's {}, I love it!") +>>> print(p) + +>>> p.parse("It's spam, I love it!") + + +("compile" is not exported for ``import *`` usage as it would override the +built-in ``compile()`` function) + +The default behaviour is to match strings case insensitively. You may match with +case by specifying `case_sensitive=True`: + +>>> parse('SPAM', 'spam', case_sensitive=True) is None +True + + +Format Syntax +------------- + +A basic version of the `Format String Syntax`_ is supported with anonymous +(fixed-position), named and formatted fields:: + + {[field name]:[format spec]} + +Field names must be a valid Python identifiers, including dotted names; +element indexes imply dictionaries (see below for example). + +Numbered fields are also not supported: the result of parsing will include +the parsed fields in the order they are parsed. + +The conversion of fields to types other than strings is done based on the +type in the format specification, which mirrors the ``format()`` behaviour. +There are no "!" field conversions like ``format()`` has. + +Some simple parse() format string examples: + +>>> parse("Bring me a {}", "Bring me a shrubbery") + +>>> r = parse("The {} who say {}", "The knights who say Ni!") +>>> print(r) + +>>> print(r.fixed) +('knights', 'Ni!') +>>> r = parse("Bring out the holy {item}", "Bring out the holy hand grenade") +>>> print(r) + +>>> print(r.named) +{'item': 'hand grenade'} +>>> print(r['item']) +hand grenade +>>> 'item' in r +True + +Note that `in` only works if you have named fields. Dotted names and indexes +are possible though the application must make additional sense of the result: + +>>> r = parse("Mmm, {food.type}, I love it!", "Mmm, spam, I love it!") +>>> print(r) + +>>> print(r.named) +{'food.type': 'spam'} +>>> print(r['food.type']) +spam +>>> r = parse("My quest is {quest[name]}", "My quest is to seek the holy grail!") +>>> print(r) + +>>> print(r['quest']) +{'name': 'to seek the holy grail!'} +>>> print(r['quest']['name']) +to seek the holy grail! + +If the text you're matching has braces in it you can match those by including +a double-brace ``{{`` or ``}}`` in your format string, just like format() does. + + +Format Specification +-------------------- + +Most often a straight format-less ``{}`` will suffice where a more complex +format specification might have been used. + +Most of `format()`'s `Format Specification Mini-Language`_ is supported: + + [[fill]align][0][width][.precision][type] + +The differences between `parse()` and `format()` are: + +- The align operators will cause spaces (or specified fill character) to be + stripped from the parsed value. The width is not enforced; it just indicates + there may be whitespace or "0"s to strip. +- Numeric parsing will automatically handle a "0b", "0o" or "0x" prefix. + That is, the "#" format character is handled automatically by d, b, o + and x formats. For "d" any will be accepted, but for the others the correct + prefix must be present if at all. +- Numeric sign is handled automatically. +- The thousands separator is handled automatically if the "n" type is used. +- The types supported are a slightly different mix to the format() types. Some + format() types come directly over: "d", "n", "%", "f", "e", "b", "o" and "x". + In addition some regular expression character group types "D", "w", "W", "s" + and "S" are also available. +- The "e" and "g" types are case-insensitive so there is not need for + the "E" or "G" types. + +===== =========================================== ======== +Type Characters Matched Output +===== =========================================== ======== +l Letters (ASCII) str +w Letters, numbers and underscore str +W Not letters, numbers and underscore str +s Whitespace str +S Non-whitespace str +d Digits (effectively integer numbers) int +D Non-digit str +n Numbers with thousands separators (, or .) int +% Percentage (converted to value/100.0) float +f Fixed-point numbers float +F Decimal numbers Decimal +e Floating-point numbers with exponent float + e.g. 1.1e-10, NAN (all case insensitive) +g General number format (either d, f or e) float +b Binary numbers int +o Octal numbers int +x Hexadecimal numbers (lower and upper case) int +ti ISO 8601 format date/time datetime + e.g. 1972-01-20T10:21:36Z ("T" and "Z" + optional) +te RFC2822 e-mail format date/time datetime + e.g. Mon, 20 Jan 1972 10:21:36 +1000 +tg Global (day/month) format date/time datetime + e.g. 20/1/1972 10:21:36 AM +1:00 +ta US (month/day) format date/time datetime + e.g. 1/20/1972 10:21:36 PM +10:30 +tc ctime() format date/time datetime + e.g. Sun Sep 16 01:03:52 1973 +th HTTP log format date/time datetime + e.g. 21/Nov/2011:00:07:11 +0000 +ts Linux system log format date/time datetime + e.g. Nov 9 03:37:44 +tt Time time + e.g. 10:21:36 PM -5:30 +===== =========================================== ======== + +Some examples of typed parsing with ``None`` returned if the typing +does not match: + +>>> parse('Our {:d} {:w} are...', 'Our 3 weapons are...') + +>>> parse('Our {:d} {:w} are...', 'Our three weapons are...') +>>> parse('Meet at {:tg}', 'Meet at 1/2/2011 11:00 PM') + + +And messing about with alignment: + +>>> parse('with {:>} herring', 'with a herring') + +>>> parse('spam {:^} spam', 'spam lovely spam') + + +Note that the "center" alignment does not test to make sure the value is +centered - it just strips leading and trailing whitespace. + +Width and precision may be used to restrict the size of matched text +from the input. Width specifies a minimum size and precision specifies +a maximum. For example: + +>>> parse('{:.2}{:.2}', 'look') # specifying precision + +>>> parse('{:4}{:4}', 'look at that') # specifying width + +>>> parse('{:4}{:.4}', 'look at that') # specifying both + +>>> parse('{:2d}{:2d}', '0440') # parsing two contiguous numbers + + +Some notes for the date and time types: + +- the presence of the time part is optional (including ISO 8601, starting + at the "T"). A full datetime object will always be returned; the time + will be set to 00:00:00. You may also specify a time without seconds. +- when a seconds amount is present in the input fractions will be parsed + to give microseconds. +- except in ISO 8601 the day and month digits may be 0-padded. +- the date separator for the tg and ta formats may be "-" or "/". +- named months (abbreviations or full names) may be used in the ta and tg + formats in place of numeric months. +- as per RFC 2822 the e-mail format may omit the day (and comma), and the + seconds but nothing else. +- hours greater than 12 will be happily accepted. +- the AM/PM are optional, and if PM is found then 12 hours will be added + to the datetime object's hours amount - even if the hour is greater + than 12 (for consistency.) +- in ISO 8601 the "Z" (UTC) timezone part may be a numeric offset +- timezones are specified as "+HH:MM" or "-HH:MM". The hour may be one or two + digits (0-padded is OK.) Also, the ":" is optional. +- the timezone is optional in all except the e-mail format (it defaults to + UTC.) +- named timezones are not handled yet. + +Note: attempting to match too many datetime fields in a single parse() will +currently result in a resource allocation issue. A TooManyFields exception +will be raised in this instance. The current limit is about 15. It is hoped +that this limit will be removed one day. + +.. _`Format String Syntax`: + http://docs.python.org/library/string.html#format-string-syntax +.. _`Format Specification Mini-Language`: + http://docs.python.org/library/string.html#format-specification-mini-language + + +Result and Match Objects +------------------------ + +The result of a ``parse()`` and ``search()`` operation is either ``None`` (no match), a +``Result`` instance or a ``Match`` instance if ``evaluate_result`` is False. + +The ``Result`` instance has three attributes: + +fixed + A tuple of the fixed-position, anonymous fields extracted from the input. +named + A dictionary of the named fields extracted from the input. +spans + A dictionary mapping the names and fixed position indices matched to a + 2-tuple slice range of where the match occurred in the input. + The span does not include any stripped padding (alignment or width). + +The ``Match`` instance has one method: + +evaluate_result() + Generates and returns a ``Result`` instance for this ``Match`` object. + + + +Custom Type Conversions +----------------------- + +If you wish to have matched fields automatically converted to your own type you +may pass in a dictionary of type conversion information to ``parse()`` and +``compile()``. + +The converter will be passed the field string matched. Whatever it returns +will be substituted in the ``Result`` instance for that field. + +Your custom type conversions may override the builtin types if you supply one +with the same identifier. + +>>> def shouty(string): +... return string.upper() +... +>>> parse('{:shouty} world', 'hello world', dict(shouty=shouty)) + + +If the type converter has the optional ``pattern`` attribute, it is used as +regular expression for better pattern matching (instead of the default one). + +>>> def parse_number(text): +... return int(text) +>>> parse_number.pattern = r'\d+' +>>> parse('Answer: {number:Number}', 'Answer: 42', dict(Number=parse_number)) + +>>> _ = parse('Answer: {:Number}', 'Answer: Alice', dict(Number=parse_number)) +>>> assert _ is None, "MISMATCH" + +You can also use the ``with_pattern(pattern)`` decorator to add this +information to a type converter function: + +>>> from parse import with_pattern +>>> @with_pattern(r'\d+') +... def parse_number(text): +... return int(text) +>>> parse('Answer: {number:Number}', 'Answer: 42', dict(Number=parse_number)) + + +A more complete example of a custom type might be: + +>>> yesno_mapping = { +... "yes": True, "no": False, +... "on": True, "off": False, +... "true": True, "false": False, +... } +>>> @with_pattern(r"|".join(yesno_mapping)) +... def parse_yesno(text): +... return yesno_mapping[text.lower()] + + +If the type converter ``pattern`` uses regex-grouping (with parenthesis), +you should indicate this by using the optional ``regex_group_count`` parameter +in the ``with_pattern()`` decorator: + +>>> @with_pattern(r'((\d+))', regex_group_count=2) +... def parse_number2(text): +... return int(text) +>>> parse('Answer: {:Number2} {:Number2}', 'Answer: 42 43', dict(Number2=parse_number2)) + + +Otherwise, this may cause parsing problems with unnamed/fixed parameters. + + +Potential Gotchas +----------------- + +`parse()` will always match the shortest text necessary (from left to right) +to fulfil the parse pattern, so for example: + +>>> pattern = '{dir1}/{dir2}' +>>> data = 'root/parent/subdir' +>>> sorted(parse(pattern, data).named.items()) +[('dir1', 'root'), ('dir2', 'parent/subdir')] + +So, even though `{'dir1': 'root/parent', 'dir2': 'subdir'}` would also fit +the pattern, the actual match represents the shortest successful match for +`dir1`. + +---- + +**Version history (in brief)**: + +- 1.11.0 Implement `__contains__` for Result instances. +- 1.10.0 Introduce a "letters" matcher, since "w" matches numbers + also. +- 1.9.1 Fix deprecation warnings around backslashes in regex strings + (thanks Mickaƫl Schoentgen). Also fix some documentation formatting + issues. +- 1.9.0 We now honor precision and width specifiers when parsing numbers + and strings, allowing parsing of concatenated elements of fixed width + (thanks Julia Signell) +- 1.8.4 Add LICENSE file at request of packagers. + Correct handling of AM/PM to follow most common interpretation. + Correct parsing of hexadecimal that looks like a binary prefix. + Add ability to parse case sensitively. + Add parsing of numbers to Decimal with "F" (thanks John Vandenberg) +- 1.8.3 Add regex_group_count to with_pattern() decorator to support + user-defined types that contain brackets/parenthesis (thanks Jens Engel) +- 1.8.2 add documentation for including braces in format string +- 1.8.1 ensure bare hexadecimal digits are not matched +- 1.8.0 support manual control over result evaluation (thanks Timo Furrer) +- 1.7.0 parse dict fields (thanks Mark Visser) and adapted to allow + more than 100 re groups in Python 3.5+ (thanks David King) +- 1.6.6 parse Linux system log dates (thanks Alex Cowan) +- 1.6.5 handle precision in float format (thanks Levi Kilcher) +- 1.6.4 handle pipe "|" characters in parse string (thanks Martijn Pieters) +- 1.6.3 handle repeated instances of named fields, fix bug in PM time + overflow +- 1.6.2 fix logging to use local, not root logger (thanks Necku) +- 1.6.1 be more flexible regarding matched ISO datetimes and timezones in + general, fix bug in timezones without ":" and improve docs +- 1.6.0 add support for optional ``pattern`` attribute in user-defined types + (thanks Jens Engel) +- 1.5.3 fix handling of question marks +- 1.5.2 fix type conversion error with dotted names (thanks Sebastian Thiel) +- 1.5.1 implement handling of named datetime fields +- 1.5 add handling of dotted field names (thanks Sebastian Thiel) +- 1.4.1 fix parsing of "0" in int conversion (thanks James Rowe) +- 1.4 add __getitem__ convenience access on Result. +- 1.3.3 fix Python 2.5 setup.py issue. +- 1.3.2 fix Python 3.2 setup.py issue. +- 1.3.1 fix a couple of Python 3.2 compatibility issues. +- 1.3 added search() and findall(); removed compile() from ``import *`` + export as it overwrites builtin. +- 1.2 added ability for custom and override type conversions to be + provided; some cleanup +- 1.1.9 to keep things simpler number sign is handled automatically; + significant robustification in the face of edge-case input. +- 1.1.8 allow "d" fields to have number base "0x" etc. prefixes; + fix up some field type interactions after stress-testing the parser; + implement "%" type. +- 1.1.7 Python 3 compatibility tweaks (2.5 to 2.7 and 3.2 are supported). +- 1.1.6 add "e" and "g" field types; removed redundant "h" and "X"; + removed need for explicit "#". +- 1.1.5 accept textual dates in more places; Result now holds match span + positions. +- 1.1.4 fixes to some int type conversion; implemented "=" alignment; added + date/time parsing with a variety of formats handled. +- 1.1.3 type conversion is automatic based on specified field types. Also added + "f" and "n" types. +- 1.1.2 refactored, added compile() and limited ``from parse import *`` +- 1.1.1 documentation improvements +- 1.1.0 implemented more of the `Format Specification Mini-Language`_ + and removed the restriction on mixing fixed-position and named fields +- 1.0.0 initial release + +This code is copyright 2012-2019 Richard Jones +See the end of the source file for the license of use. +''' + +from __future__ import absolute_import +__version__ = '1.11.0' + +# yes, I now have two problems +import re +import sys +from datetime import datetime, time, tzinfo, timedelta +from decimal import Decimal +from functools import partial +import logging + +__all__ = 'parse search findall with_pattern'.split() + +log = logging.getLogger(__name__) + + +def with_pattern(pattern, regex_group_count=None): + """Attach a regular expression pattern matcher to a custom type converter + function. + + This annotates the type converter with the :attr:`pattern` attribute. + + EXAMPLE: + >>> import parse + >>> @parse.with_pattern(r"\d+") + ... def parse_number(text): + ... return int(text) + + is equivalent to: + + >>> def parse_number(text): + ... return int(text) + >>> parse_number.pattern = r"\d+" + + :param pattern: regular expression pattern (as text) + :param regex_group_count: Indicates how many regex-groups are in pattern. + :return: wrapped function + """ + def decorator(func): + func.pattern = pattern + func.regex_group_count = regex_group_count + return func + return decorator + + +def int_convert(base): + '''Convert a string to an integer. + + The string may start with a sign. + + It may be of a base other than 10. + + If may start with a base indicator, 0#nnnn, which we assume should + override the specified base. + + It may also have other non-numeric characters that we can ignore. + ''' + CHARS = '0123456789abcdefghijklmnopqrstuvwxyz' + + def f(string, match, base=base): + if string[0] == '-': + sign = -1 + else: + sign = 1 + + if string[0] == '0' and len(string) > 2: + if string[1] in 'bB': + base = 2 + elif string[1] in 'oO': + base = 8 + elif string[1] in 'xX': + base = 16 + else: + # just go with the base specifed + pass + + chars = CHARS[:base] + string = re.sub('[^%s]' % chars, '', string.lower()) + return sign * int(string, base) + return f + + +def percentage(string, match): + return float(string[:-1]) / 100. + + +class FixedTzOffset(tzinfo): + """Fixed offset in minutes east from UTC. + """ + ZERO = timedelta(0) + + def __init__(self, offset, name): + self._offset = timedelta(minutes=offset) + self._name = name + + def __repr__(self): + return '<%s %s %s>' % (self.__class__.__name__, self._name, + self._offset) + + def utcoffset(self, dt): + return self._offset + + def tzname(self, dt): + return self._name + + def dst(self, dt): + return self.ZERO + + def __eq__(self, other): + return self._name == other._name and self._offset == other._offset + + +MONTHS_MAP = dict( + Jan=1, January=1, + Feb=2, February=2, + Mar=3, March=3, + Apr=4, April=4, + May=5, + Jun=6, June=6, + Jul=7, July=7, + Aug=8, August=8, + Sep=9, September=9, + Oct=10, October=10, + Nov=11, November=11, + Dec=12, December=12 +) +DAYS_PAT = r'(Mon|Tue|Wed|Thu|Fri|Sat|Sun)' +MONTHS_PAT = r'(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)' +ALL_MONTHS_PAT = r'(%s)' % '|'.join(MONTHS_MAP) +TIME_PAT = r'(\d{1,2}:\d{1,2}(:\d{1,2}(\.\d+)?)?)' +AM_PAT = r'(\s+[AP]M)' +TZ_PAT = r'(\s+[-+]\d\d?:?\d\d)' + + +def date_convert(string, match, ymd=None, mdy=None, dmy=None, + d_m_y=None, hms=None, am=None, tz=None, mm=None, dd=None): + '''Convert the incoming string containing some date / time info into a + datetime instance. + ''' + groups = match.groups() + time_only = False + if mm and dd: + y=datetime.today().year + m=groups[mm] + d=groups[dd] + elif ymd is not None: + y, m, d = re.split(r'[-/\s]', groups[ymd]) + elif mdy is not None: + m, d, y = re.split(r'[-/\s]', groups[mdy]) + elif dmy is not None: + d, m, y = re.split(r'[-/\s]', groups[dmy]) + elif d_m_y is not None: + d, m, y = d_m_y + d = groups[d] + m = groups[m] + y = groups[y] + else: + time_only = True + + H = M = S = u = 0 + if hms is not None and groups[hms]: + t = groups[hms].split(':') + if len(t) == 2: + H, M = t + else: + H, M, S = t + if '.' in S: + S, u = S.split('.') + u = int(float('.' + u) * 1000000) + S = int(S) + H = int(H) + M = int(M) + + if am is not None: + am = groups[am] + if am: + am = am.strip() + if am == 'AM' and H == 12: + # correction for "12" hour functioning as "0" hour: 12:15 AM = 00:15 by 24 hr clock + H -= 12 + elif am == 'PM' and H == 12: + # no correction needed: 12PM is midday, 12:00 by 24 hour clock + pass + elif am == 'PM': + H += 12 + + if tz is not None: + tz = groups[tz] + if tz == 'Z': + tz = FixedTzOffset(0, 'UTC') + elif tz: + tz = tz.strip() + if tz.isupper(): + # TODO use the awesome python TZ module? + pass + else: + sign = tz[0] + if ':' in tz: + tzh, tzm = tz[1:].split(':') + elif len(tz) == 4: # 'snnn' + tzh, tzm = tz[1], tz[2:4] + else: + tzh, tzm = tz[1:3], tz[3:5] + offset = int(tzm) + int(tzh) * 60 + if sign == '-': + offset = -offset + tz = FixedTzOffset(offset, tz) + + if time_only: + d = time(H, M, S, u, tzinfo=tz) + else: + y = int(y) + if m.isdigit(): + m = int(m) + else: + m = MONTHS_MAP[m] + d = int(d) + d = datetime(y, m, d, H, M, S, u, tzinfo=tz) + + return d + + +class TooManyFields(ValueError): + pass + + +class RepeatedNameError(ValueError): + pass + + +# note: {} are handled separately +# note: I don't use r'' here because Sublime Text 2 syntax highlight has a fit +REGEX_SAFETY = re.compile(r'([?\\\\.[\]()*+\^$!\|])') + +# allowed field types +ALLOWED_TYPES = set(list('nbox%fFegwWdDsSl') + + ['t' + c for c in 'ieahgcts']) + + +def extract_format(format, extra_types): + '''Pull apart the format [[fill]align][0][width][.precision][type] + ''' + fill = align = None + if format[0] in '<>=^': + align = format[0] + format = format[1:] + elif len(format) > 1 and format[1] in '<>=^': + fill = format[0] + align = format[1] + format = format[2:] + + zero = False + if format and format[0] == '0': + zero = True + format = format[1:] + + width = '' + while format: + if not format[0].isdigit(): + break + width += format[0] + format = format[1:] + + if format.startswith('.'): + # Precision isn't needed but we need to capture it so that + # the ValueError isn't raised. + format = format[1:] # drop the '.' + precision = '' + while format: + if not format[0].isdigit(): + break + precision += format[0] + format = format[1:] + + # the rest is the type, if present + type = format + if type and type not in ALLOWED_TYPES and type not in extra_types: + raise ValueError('format spec %r not recognised' % type) + + return locals() + + +PARSE_RE = re.compile(r"""({{|}}|{\w*(?:(?:\.\w+)|(?:\[[^\]]+\]))*(?::[^}]+)?})""") + + +class Parser(object): + '''Encapsulate a format string that may be used to parse other strings. + ''' + def __init__(self, format, extra_types=None, case_sensitive=False): + # a mapping of a name as in {hello.world} to a regex-group compatible + # name, like hello__world Its used to prevent the transformation of + # name-to-group and group to name to fail subtly, such as in: + # hello_.world-> hello___world->hello._world + self._group_to_name_map = {} + # also store the original field name to group name mapping to allow + # multiple instances of a name in the format string + self._name_to_group_map = {} + # and to sanity check the repeated instances store away the first + # field type specification for the named field + self._name_types = {} + + self._format = format + if extra_types is None: + extra_types = {} + self._extra_types = extra_types + if case_sensitive: + self._re_flags = re.DOTALL + else: + self._re_flags = re.IGNORECASE | re.DOTALL + self._fixed_fields = [] + self._named_fields = [] + self._group_index = 0 + self._type_conversions = {} + self._expression = self._generate_expression() + self.__search_re = None + self.__match_re = None + + log.debug('format %r -> %r', format, self._expression) + + def __repr__(self): + if len(self._format) > 20: + return '<%s %r>' % (self.__class__.__name__, + self._format[:17] + '...') + return '<%s %r>' % (self.__class__.__name__, self._format) + + @property + def _search_re(self): + if self.__search_re is None: + try: + self.__search_re = re.compile(self._expression, self._re_flags) + except AssertionError: + # access error through sys to keep py3k and backward compat + e = str(sys.exc_info()[1]) + if e.endswith('this version only supports 100 named groups'): + raise TooManyFields('sorry, you are attempting to parse ' + 'too many complex fields') + return self.__search_re + + @property + def _match_re(self): + if self.__match_re is None: + expression = r'^%s$' % self._expression + try: + self.__match_re = re.compile(expression, self._re_flags) + except AssertionError: + # access error through sys to keep py3k and backward compat + e = str(sys.exc_info()[1]) + if e.endswith('this version only supports 100 named groups'): + raise TooManyFields('sorry, you are attempting to parse ' + 'too many complex fields') + except re.error: + raise NotImplementedError("Group names (e.g. (?P) can " + "cause failure, as they are not escaped properly: '%s'" % + expression) + return self.__match_re + + def parse(self, string, evaluate_result=True): + '''Match my format to the string exactly. + + Return a Result or Match instance or None if there's no match. + ''' + m = self._match_re.match(string) + if m is None: + return None + + if evaluate_result: + return self.evaluate_result(m) + else: + return Match(self, m) + + def search(self, string, pos=0, endpos=None, evaluate_result=True): + '''Search the string for my format. + + Optionally start the search at "pos" character index and limit the + search to a maximum index of endpos - equivalent to + search(string[:endpos]). + + If the ``evaluate_result`` argument is set to ``False`` a + Match instance is returned instead of the actual Result instance. + + Return either a Result instance or None if there's no match. + ''' + if endpos is None: + endpos = len(string) + m = self._search_re.search(string, pos, endpos) + if m is None: + return None + + if evaluate_result: + return self.evaluate_result(m) + else: + return Match(self, m) + + def findall(self, string, pos=0, endpos=None, extra_types=None, evaluate_result=True): + '''Search "string" for all occurrences of "format". + + Optionally start the search at "pos" character index and limit the + search to a maximum index of endpos - equivalent to + search(string[:endpos]). + + Returns an iterator that holds Result or Match instances for each format match + found. + ''' + if endpos is None: + endpos = len(string) + return ResultIterator(self, string, pos, endpos, evaluate_result=evaluate_result) + + def _expand_named_fields(self, named_fields): + result = {} + for field, value in named_fields.items(): + # split 'aaa[bbb][ccc]...' into 'aaa' and '[bbb][ccc]...' + basename, subkeys = re.match(r'([^\[]+)(.*)', field).groups() + + # create nested dictionaries {'aaa': {'bbb': {'ccc': ...}}} + d = result + k = basename + + if subkeys: + for subkey in re.findall(r'\[[^\]]+\]', subkeys): + d = d.setdefault(k,{}) + k = subkey[1:-1] + + # assign the value to the last key + d[k] = value + + return result + + def evaluate_result(self, m): + '''Generate a Result instance for the given regex match object''' + # ok, figure the fixed fields we've pulled out and type convert them + fixed_fields = list(m.groups()) + for n in self._fixed_fields: + if n in self._type_conversions: + fixed_fields[n] = self._type_conversions[n](fixed_fields[n], m) + fixed_fields = tuple(fixed_fields[n] for n in self._fixed_fields) + + # grab the named fields, converting where requested + groupdict = m.groupdict() + named_fields = {} + name_map = {} + for k in self._named_fields: + korig = self._group_to_name_map[k] + name_map[korig] = k + if k in self._type_conversions: + value = self._type_conversions[k](groupdict[k], m) + else: + value = groupdict[k] + + named_fields[korig] = value + + # now figure the match spans + spans = dict((n, m.span(name_map[n])) for n in named_fields) + spans.update((i, m.span(n + 1)) + for i, n in enumerate(self._fixed_fields)) + + # and that's our result + return Result(fixed_fields, self._expand_named_fields(named_fields), spans) + + def _regex_replace(self, match): + return '\\' + match.group(1) + + def _generate_expression(self): + # turn my _format attribute into the _expression attribute + e = [] + for part in PARSE_RE.split(self._format): + if not part: + continue + elif part == '{{': + e.append(r'\{') + elif part == '}}': + e.append(r'\}') + elif part[0] == '{': + # this will be a braces-delimited field to handle + e.append(self._handle_field(part)) + else: + # just some text to match + e.append(REGEX_SAFETY.sub(self._regex_replace, part)) + return ''.join(e) + + def _to_group_name(self, field): + # return a version of field which can be used as capture group, even + # though it might contain '.' + group = field.replace('.', '_').replace('[', '_').replace(']', '_') + + # make sure we don't collide ("a.b" colliding with "a_b") + n = 1 + while group in self._group_to_name_map: + n += 1 + if '.' in field: + group = field.replace('.', '_' * n) + elif '_' in field: + group = field.replace('_', '_' * n) + else: + raise KeyError('duplicated group name %r' % (field,)) + + # save off the mapping + self._group_to_name_map[group] = field + self._name_to_group_map[field] = group + return group + + def _handle_field(self, field): + # first: lose the braces + field = field[1:-1] + + # now figure whether this is an anonymous or named field, and whether + # there's any format specification + format = '' + if field and field[0].isalpha(): + if ':' in field: + name, format = field.split(':') + else: + name = field + if name in self._name_to_group_map: + if self._name_types[name] != format: + raise RepeatedNameError('field type %r for field "%s" ' + 'does not match previous seen type %r' % (format, + name, self._name_types[name])) + group = self._name_to_group_map[name] + # match previously-seen value + return r'(?P=%s)' % group + else: + group = self._to_group_name(name) + self._name_types[name] = format + self._named_fields.append(group) + # this will become a group, which must not contain dots + wrap = r'(?P<%s>%%s)' % group + else: + self._fixed_fields.append(self._group_index) + wrap = r'(%s)' + if ':' in field: + format = field[1:] + group = self._group_index + + # simplest case: no type specifier ({} or {name}) + if not format: + self._group_index += 1 + return wrap % r'.+?' + + # decode the format specification + format = extract_format(format, self._extra_types) + + # figure type conversions, if any + type = format['type'] + is_numeric = type and type in 'n%fegdobh' + if type in self._extra_types: + type_converter = self._extra_types[type] + s = getattr(type_converter, 'pattern', r'.+?') + regex_group_count = getattr(type_converter, 'regex_group_count', 0) + if regex_group_count is None: + regex_group_count = 0 + self._group_index += regex_group_count + + def f(string, m): + return type_converter(string) + self._type_conversions[group] = f + elif type == 'n': + s = r'\d{1,3}([,.]\d{3})*' + self._group_index += 1 + self._type_conversions[group] = int_convert(10) + elif type == 'b': + s = r'(0[bB])?[01]+' + self._type_conversions[group] = int_convert(2) + self._group_index += 1 + elif type == 'o': + s = r'(0[oO])?[0-7]+' + self._type_conversions[group] = int_convert(8) + self._group_index += 1 + elif type == 'x': + s = r'(0[xX])?[0-9a-fA-F]+' + self._type_conversions[group] = int_convert(16) + self._group_index += 1 + elif type == '%': + s = r'\d+(\.\d+)?%' + self._group_index += 1 + self._type_conversions[group] = percentage + elif type == 'f': + s = r'\d+\.\d+' + self._type_conversions[group] = lambda s, m: float(s) + elif type == 'F': + s = r'\d+\.\d+' + self._type_conversions[group] = lambda s, m: Decimal(s) + elif type == 'e': + s = r'\d+\.\d+[eE][-+]?\d+|nan|NAN|[-+]?inf|[-+]?INF' + self._type_conversions[group] = lambda s, m: float(s) + elif type == 'g': + s = r'\d+(\.\d+)?([eE][-+]?\d+)?|nan|NAN|[-+]?inf|[-+]?INF' + self._group_index += 2 + self._type_conversions[group] = lambda s, m: float(s) + elif type == 'd': + if format.get('width'): + width = r'{1,%s}' % int(format['width']) + else: + width = '+' + s = r'\d{w}|0[xX][0-9a-fA-F]{w}|0[bB][01]{w}|0[oO][0-7]{w}'.format(w=width) + self._type_conversions[group] = int_convert(10) + elif type == 'ti': + s = r'(\d{4}-\d\d-\d\d)((\s+|T)%s)?(Z|\s*[-+]\d\d:?\d\d)?' % \ + TIME_PAT + n = self._group_index + self._type_conversions[group] = partial(date_convert, ymd=n + 1, + hms=n + 4, tz=n + 7) + self._group_index += 7 + elif type == 'tg': + s = r'(\d{1,2}[-/](\d{1,2}|%s)[-/]\d{4})(\s+%s)?%s?%s?' % ( + ALL_MONTHS_PAT, TIME_PAT, AM_PAT, TZ_PAT) + n = self._group_index + self._type_conversions[group] = partial(date_convert, dmy=n + 1, + hms=n + 5, am=n + 8, tz=n + 9) + self._group_index += 9 + elif type == 'ta': + s = r'((\d{1,2}|%s)[-/]\d{1,2}[-/]\d{4})(\s+%s)?%s?%s?' % ( + ALL_MONTHS_PAT, TIME_PAT, AM_PAT, TZ_PAT) + n = self._group_index + self._type_conversions[group] = partial(date_convert, mdy=n + 1, + hms=n + 5, am=n + 8, tz=n + 9) + self._group_index += 9 + elif type == 'te': + # this will allow microseconds through if they're present, but meh + s = r'(%s,\s+)?(\d{1,2}\s+%s\s+\d{4})\s+%s%s' % (DAYS_PAT, + MONTHS_PAT, TIME_PAT, TZ_PAT) + n = self._group_index + self._type_conversions[group] = partial(date_convert, dmy=n + 3, + hms=n + 5, tz=n + 8) + self._group_index += 8 + elif type == 'th': + # slight flexibility here from the stock Apache format + s = r'(\d{1,2}[-/]%s[-/]\d{4}):%s%s' % (MONTHS_PAT, TIME_PAT, + TZ_PAT) + n = self._group_index + self._type_conversions[group] = partial(date_convert, dmy=n + 1, + hms=n + 3, tz=n + 6) + self._group_index += 6 + elif type == 'tc': + s = r'(%s)\s+%s\s+(\d{1,2})\s+%s\s+(\d{4})' % ( + DAYS_PAT, MONTHS_PAT, TIME_PAT) + n = self._group_index + self._type_conversions[group] = partial(date_convert, + d_m_y=(n + 4, n + 3, n + 8), hms=n + 5) + self._group_index += 8 + elif type == 'tt': + s = r'%s?%s?%s?' % (TIME_PAT, AM_PAT, TZ_PAT) + n = self._group_index + self._type_conversions[group] = partial(date_convert, hms=n + 1, + am=n + 4, tz=n + 5) + self._group_index += 5 + elif type == 'ts': + s = r'%s(\s+)(\d+)(\s+)(\d{1,2}:\d{1,2}:\d{1,2})?' % MONTHS_PAT + n = self._group_index + self._type_conversions[group] = partial(date_convert, mm=n+1, dd=n+3, + hms=n + 5) + self._group_index += 5 + elif type == 'l': + s = r'[A-Za-z]+' + elif type: + s = r'\%s+' % type + elif format.get('precision'): + if format.get('width'): + s = r'.{%s,%s}?' % (format['width'], format['precision']) + else: + s = r'.{1,%s}?' % format['precision'] + elif format.get('width'): + s = r'.{%s,}?' % format['width'] + else: + s = r'.+?' + + align = format['align'] + fill = format['fill'] + + # handle some numeric-specific things like fill and sign + if is_numeric: + # prefix with something (align "=" trumps zero) + if align == '=': + # special case - align "=" acts like the zero above but with + # configurable fill defaulting to "0" + if not fill: + fill = '0' + s = r'%s*' % fill + s + + # allow numbers to be prefixed with a sign + s = r'[-+ ]?' + s + + if not fill: + fill = ' ' + + # Place into a group now - this captures the value we want to keep. + # Everything else from now is just padding to be stripped off + if wrap: + s = wrap % s + self._group_index += 1 + + if format['width']: + # all we really care about is that if the format originally + # specified a width then there will probably be padding - without + # an explicit alignment that'll mean right alignment with spaces + # padding + if not align: + align = '>' + + if fill in r'.\+?*[](){}^$': + fill = '\\' + fill + + # align "=" has been handled + if align == '<': + s = '%s%s*' % (s, fill) + elif align == '>': + s = '%s*%s' % (fill, s) + elif align == '^': + s = '%s*%s%s*' % (fill, s, fill) + + return s + + +class Result(object): + '''The result of a parse() or search(). + + Fixed results may be looked up using `result[index]`. + + Named results may be looked up using `result['name']`. + + Named results may be tested for existence using `'name' in result`. + ''' + def __init__(self, fixed, named, spans): + self.fixed = fixed + self.named = named + self.spans = spans + + def __getitem__(self, item): + if isinstance(item, int): + return self.fixed[item] + return self.named[item] + + def __repr__(self): + return '<%s %r %r>' % (self.__class__.__name__, self.fixed, + self.named) + + def __contains__(self, name): + return name in self.named + + +class Match(object): + '''The result of a parse() or search() if no results are generated. + + This class is only used to expose internal used regex match objects + to the user and use them for external Parser.evaluate_result calls. + ''' + def __init__(self, parser, match): + self.parser = parser + self.match = match + + def evaluate_result(self): + '''Generate results for this Match''' + return self.parser.evaluate_result(self.match) + + +class ResultIterator(object): + '''The result of a findall() operation. + + Each element is a Result instance. + ''' + def __init__(self, parser, string, pos, endpos, evaluate_result=True): + self.parser = parser + self.string = string + self.pos = pos + self.endpos = endpos + self.evaluate_result = evaluate_result + + def __iter__(self): + return self + + def __next__(self): + m = self.parser._search_re.search(self.string, self.pos, self.endpos) + if m is None: + raise StopIteration() + self.pos = m.end() + + if self.evaluate_result: + return self.parser.evaluate_result(m) + else: + return Match(self.parser, m) + + # pre-py3k compat + next = __next__ + + +def parse(format, string, extra_types=None, evaluate_result=True, case_sensitive=False): + '''Using "format" attempt to pull values from "string". + + The format must match the string contents exactly. If the value + you're looking for is instead just a part of the string use + search(). + + If ``evaluate_result`` is True the return value will be an Result instance with two attributes: + + .fixed - tuple of fixed-position values from the string + .named - dict of named values from the string + + If ``evaluate_result`` is False the return value will be a Match instance with one method: + + .evaluate_result() - This will return a Result instance like you would get + with ``evaluate_result`` set to True + + The default behaviour is to match strings case insensitively. You may match with + case by specifying case_sensitive=True. + + If the format is invalid a ValueError will be raised. + + See the module documentation for the use of "extra_types". + + In the case there is no match parse() will return None. + ''' + p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) + return p.parse(string, evaluate_result=evaluate_result) + + +def search(format, string, pos=0, endpos=None, extra_types=None, evaluate_result=True, + case_sensitive=False): + '''Search "string" for the first occurrence of "format". + + The format may occur anywhere within the string. If + instead you wish for the format to exactly match the string + use parse(). + + Optionally start the search at "pos" character index and limit the search + to a maximum index of endpos - equivalent to search(string[:endpos]). + + If ``evaluate_result`` is True the return value will be an Result instance with two attributes: + + .fixed - tuple of fixed-position values from the string + .named - dict of named values from the string + + If ``evaluate_result`` is False the return value will be a Match instance with one method: + + .evaluate_result() - This will return a Result instance like you would get + with ``evaluate_result`` set to True + + The default behaviour is to match strings case insensitively. You may match with + case by specifying case_sensitive=True. + + If the format is invalid a ValueError will be raised. + + See the module documentation for the use of "extra_types". + + In the case there is no match parse() will return None. + ''' + p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) + return p.search(string, pos, endpos, evaluate_result=evaluate_result) + + +def findall(format, string, pos=0, endpos=None, extra_types=None, evaluate_result=True, + case_sensitive=False): + '''Search "string" for all occurrences of "format". + + You will be returned an iterator that holds Result instances + for each format match found. + + Optionally start the search at "pos" character index and limit the search + to a maximum index of endpos - equivalent to search(string[:endpos]). + + If ``evaluate_result`` is True each returned Result instance has two attributes: + + .fixed - tuple of fixed-position values from the string + .named - dict of named values from the string + + If ``evaluate_result`` is False each returned value is a Match instance with one method: + + .evaluate_result() - This will return a Result instance like you would get + with ``evaluate_result`` set to True + + The default behaviour is to match strings case insensitively. You may match with + case by specifying case_sensitive=True. + + If the format is invalid a ValueError will be raised. + + See the module documentation for the use of "extra_types". + ''' + p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) + return Parser(format, extra_types=extra_types).findall(string, pos, endpos, evaluate_result=evaluate_result) + + +def compile(format, extra_types=None, case_sensitive=False): + '''Create a Parser instance to parse "format". + + The resultant Parser has a method .parse(string) which + behaves in the same manner as parse(format, string). + + The default behaviour is to match strings case insensitively. You may match with + case by specifying case_sensitive=True. + + Use this function if you intend to parse many strings + with the same format. + + See the module documentation for the use of "extra_types". + + Returns a Parser instance. + ''' + return Parser(format, extra_types=extra_types) + + +# Copyright (c) 2012-2019 Richard Jones +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# vim: set filetype=python ts=4 sw=4 et si tw=75 diff --git a/devel/setup.cfg b/devel/setup.cfg new file mode 100644 index 0000000..3bf77b8 --- /dev/null +++ b/devel/setup.cfg @@ -0,0 +1,8 @@ +[flake8] +# flake8 is an automated code formatting pedant. +# Use it, please. +# +# python3 -m flake8 . +# +ignore = E501 +exclude = .git \ No newline at end of file diff --git a/devel/update-words.sh b/devel/update-words.sh new file mode 100755 index 0000000..f64d1cb --- /dev/null +++ b/devel/update-words.sh @@ -0,0 +1,19 @@ +#!/bin/sh +set +e + +url='https://rawgit.com/first20hours/google-10000-english/master/google-10000-english-no-swears.txt' +getter="curl -sL" +fn="answer_words.txt" + +filterer() { + grep '......*' +} + +if ! curl -h >/dev/null 2>/dev/null; then + getter="wget -q -O -" +elif ! wget -h >/dev/null 2>/dev/null; then + echo "[!] I don't know how to download. I need curl or wget." +fi + +$getter "${url}" | filterer > ${fn}.tmp \ + && mv -f ${fn}.tmp ${fn} diff --git a/devel/validate.py b/devel/validate.py new file mode 100644 index 0000000..d73dd89 --- /dev/null +++ b/devel/validate.py @@ -0,0 +1,229 @@ +#!/usr/bin/python3 + +"""A validator for MOTH puzzles""" + +import logging +import os +import os.path +import re + +import moth + +# pylint: disable=len-as-condition, line-too-long + +DEFAULT_REQUIRED_FIELDS = ["answers", "authors", "summary"] + +LOGGER = logging.getLogger(__name__) + + +class MothValidationError(Exception): + + """An exception for encapsulating MOTH puzzle validation errors""" + + +class MothValidator: + + """A class which validates MOTH categories""" + + def __init__(self, fields): + self.required_fields = fields + self.results = {"category": {}, "checks": []} + + def validate(self, categorydir, only_errors=False): + """Run validation checks against a category""" + LOGGER.debug("Loading category from %s", categorydir) + try: + category = moth.Category(categorydir, 0) + except NotADirectoryError: + return + + LOGGER.debug("Found %d puzzles in %s", len(category.pointvals()), categorydir) + + self.results["category"][categorydir] = { + "puzzles": {}, + "name": os.path.basename(categorydir.strip(os.sep)), + } + curr_category = self.results["category"][categorydir] + + for check_function_name in [x for x in dir(self) if x.startswith("check_") and callable(getattr(self, x))]: + if check_function_name not in self.results["checks"]: + self.results["checks"].append(check_function_name) + + for puzzle in category: + LOGGER.info("Processing %s: %s", categorydir, puzzle.points) + + curr_category["puzzles"][puzzle.points] = {} + curr_puzzle = curr_category["puzzles"][puzzle.points] + curr_puzzle["failures"] = [] + + for check_function_name in [x for x in dir(self) if x.startswith("check_") and callable(getattr(self, x))]: + check_function = getattr(self, check_function_name) + LOGGER.debug("Running %s on %d", check_function_name, puzzle.points) + + try: + check_function(puzzle) + except MothValidationError as ex: + curr_puzzle["failures"].append(str(ex)) + + if only_errors and len(curr_puzzle["failures"]) == 0: + del curr_category["puzzles"][puzzle.points] + + def check_fields(self, puzzle): + """Check if the puzzle has the requested fields""" + for field in self.required_fields: + if not hasattr(puzzle, field) or \ + getattr(puzzle,field) is None or \ + getattr(puzzle,field) == "": + raise MothValidationError("Missing field %s" % (field,)) + + @staticmethod + def check_has_answers(puzzle): + """Check if the puzle has answers defined""" + if len(puzzle.answers) == 0: + raise MothValidationError("No answers provided") + + @staticmethod + def check_unique_answers(puzzle): + """Check if puzzle answers are unique""" + known_answers = [] + duplicate_answers = [] + + for answer in puzzle.answers: + if answer not in known_answers: + known_answers.append(answer) + else: + duplicate_answers.append(answer) + + if len(duplicate_answers) > 0: + raise MothValidationError("Duplicate answer(s) %s" % ", ".join(duplicate_answers)) + + @staticmethod + def check_has_authors(puzzle): + """Check if the puzzle has authors defined""" + if len(puzzle.authors) == 0: + raise MothValidationError("No authors provided") + + @staticmethod + def check_unique_authors(puzzle): + """Check if puzzle authors are unique""" + known_authors = [] + duplicate_authors = [] + + for author in puzzle.authors: + if author not in known_authors: + known_authors.append(author) + else: + duplicate_authors.append(author) + + if len(duplicate_authors) > 0: + raise MothValidationError("Duplicate author(s) %s" % ", ".join(duplicate_authors)) + + @staticmethod + def check_has_summary(puzzle): + """Check if the puzzle has a summary""" + if puzzle.summary is None: + raise MothValidationError("Summary has not been provided") + + @staticmethod + def check_has_body(puzzle): + """Check if the puzzle has a body defined""" + old_pos = puzzle.body.tell() + puzzle.body.seek(0) + if len(puzzle.body.read()) == 0: + puzzle.body.seek(old_pos) + raise MothValidationError("No body provided") + + puzzle.body.seek(old_pos) + + @staticmethod + def check_ksa_format(puzzle): + """Check if KSAs are properly formatted""" + + ksa_re = re.compile("^[KSA]\d{4}$") + + if hasattr(puzzle, "ksa"): + for ksa in puzzle.ksa: + if ksa_re.match(ksa) is None: + raise MothValidationError("Unrecognized KSA format (%s)" % (ksa,)) + + @staticmethod + def check_success(puzzle): + """Check if success criteria are defined""" + + if not hasattr(puzzle, "success"): + raise MothValidationError("Success not defined") + + criteria = ["acceptable", "mastery"] + missing_criteria = [] + for criterion in criteria: + if criterion not in puzzle.success.keys() or \ + puzzle.success[criterion] is None or \ + len(puzzle.success[criterion]) == 0: + missing_criteria.append(criterion) + + if len(missing_criteria) > 0: + raise MothValidationError("Missing success criteria (%s)" % (", ".join(missing_criteria))) + + +def output_json(data): + """Output results in JSON format""" + import json + print(json.dumps(data)) + + +def output_text(data): + """Output results in a text-based tabular format""" + + longest_category = max([len(y["name"]) for x, y in data["category"].items()]) + longest_category = max([longest_category, len("Category")]) + longest_failure = len("Failures") + for category_data in data["category"].values(): + for points, puzzle_data in category_data["puzzles"].items(): + longest_failure = max([longest_failure, len(", ".join(puzzle_data["failures"]))]) + + formatstr = "| %%%ds | %%6s | %%%ds |" % (longest_category, longest_failure) + headerfmt = formatstr % ("Category", "Points", "Failures") + + print(headerfmt) + for cat_data in data["category"].values(): + for points, puzzle_data in sorted(cat_data["puzzles"].items()): + print(formatstr % (cat_data["name"], points, ", ".join([str(x) for x in puzzle_data["failures"]]))) + + +def main(): + """Main function""" + # pylint: disable=invalid-name + import argparse + + LOGGER.addHandler(logging.StreamHandler()) + + parser = argparse.ArgumentParser(description="Validate MOTH puzzle field compliance") + parser.add_argument("category", nargs="+", help="Categories to validate") + parser.add_argument("-f", "--fields", help="Comma-separated list of fields to check for", default=",".join(DEFAULT_REQUIRED_FIELDS)) + + parser.add_argument("-o", "--output-format", choices=["text", "json"], default="text", help="Output format (default: text)") + parser.add_argument("-e", "--only-errors", action="store_true", default=False, help="Only output errors") + parser.add_argument("-v", "--verbose", action="count", default=0, help="Increase verbosity of output, repeat to increase") + + args = parser.parse_args() + + if args.verbose == 1: + LOGGER.setLevel("INFO") + elif args.verbose > 1: + LOGGER.setLevel("DEBUG") + + LOGGER.debug(args) + validator = MothValidator(args.fields.split(",")) + + for category in args.category: + LOGGER.info("Validating %s", category) + validator.validate(category, only_errors=args.only_errors) + + if args.output_format == "text": + output_text(validator.results) + elif args.output_format == "json": + output_json(validator.results) + + +if __name__ == "__main__": + main() diff --git a/theme/moth.js b/theme/moth.js index 029efcd..c773f21 100644 --- a/theme/moth.js +++ b/theme/moth.js @@ -98,7 +98,8 @@ function renderPuzzles(obj) { function renderState(obj) { devel = obj.Config.Devel if (devel) { - sessionStorage.id = "1234" + let params = new URLSearchParams(window.location.search) + sessionStorage.id = "1" sessionStorage.pid = "rodney" } if (Object.keys(obj.Puzzles).length > 0) {