From 6bff9bfd8d0a22a13ecd884efd8ba2ba06e33f7c Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Mon, 17 Oct 2016 12:36:40 -0600 Subject: [PATCH 01/18] Made devel server compatible with python 3.4. --- devel-server.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/devel-server.py b/devel-server.py index 410e109..f4ae8d4 100755 --- a/devel-server.py +++ b/devel-server.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import glob +import http import http.server import mistune import os @@ -8,7 +9,13 @@ import pathlib import puzzles import socketserver -HTTPStatus = http.server.HTTPStatus +if hasattr(http, 'HTTPStatus'): + HTTPStatus = http.HTTPStatus +else: + class HTTPStatus: + NOT_FOUND = 404 + OK = 200 + def page(title, body): return """ From f06de852e846631b90e6c7c195913da1abce1f68 Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Mon, 17 Oct 2016 13:24:54 -0600 Subject: [PATCH 02/18] Reworked some basics of how the Puzzle class works. Shouldn't look to different from the outside. --- .gitignore | 3 ++- devel-server.py | 3 +-- puzzles.py | 48 +++++++++++++++++++++++++++--------------------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index c3c1634..a22d658 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,9 @@ *# *.pyc *.o +.idea ./bin/ build/ cache/ target/ -puzzles \ No newline at end of file +puzzles diff --git a/devel-server.py b/devel-server.py index f4ae8d4..732a87b 100755 --- a/devel-server.py +++ b/devel-server.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 import glob -import http import http.server import mistune import os @@ -9,7 +8,7 @@ import pathlib import puzzles import socketserver -if hasattr(http, 'HTTPStatus'): +if hasattr(http.server, 'HTTPStatus'): HTTPStatus = http.HTTPStatus else: class HTTPStatus: diff --git a/puzzles.py b/puzzles.py index e2c79f5..e844a00 100644 --- a/puzzles.py +++ b/puzzles.py @@ -1,12 +1,13 @@ #!/usr/bin/python3 -import hmac -import base64 import argparse +import base64 import glob +import hmac import json -import os import mistune +import multidict +import os import random messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' @@ -17,12 +18,19 @@ def djb2hash(buf): h = ((h * 33) + c) & 0xffffffff return h -class Puzzle: - def __init__(self, stream): +class Puzzle(multidict.MultiDict): + + def __init__(self, seed): + super().__init__() + self.message = bytes(random.choice(messageChars) for i in range(20)) - self.fields = {} - self.answers = [] - self.hashes = [] + self.body = '' + + self.rand = random.Random(seed) + + @classmethod + def from_stream(cls, stream): + pzl = cls(None) body = [] header = True @@ -35,34 +43,32 @@ class Puzzle: key, val = line.split(':', 1) key = key.lower() val = val.strip() - self._add_field(key, val) + pzl.add(key, val) else: body.append(line) - self.body = ''.join(body) + pzl.body = ''.join(body) + return pzl - def _add_field(self, key, val): + def add(self, key, value): + super().add(key, value) if key == 'answer': - h = djb2hash(val.encode('utf8')) - self.answers.append(val) - self.hashes.append(h) - else: - self.fields[key] = val + super().add(hash, djb2hash(value.encode('utf8'))) def htmlify(self): return mistune.markdown(self.body) def publish(self): obj = { - 'author': self.fields['author'], - 'hashes': self.hashes, + 'author': self['author'], + 'hashes': self.getall('hash'), 'body': self.htmlify(), } return obj def secrets(self): obj = { - 'answers': self.answers, - 'summary': self.fields['summary'], + 'answers': self.getall('answer'), + 'summary': self['summary'], } return obj @@ -78,7 +84,7 @@ if __name__ == '__main__': filename = os.path.basename(puzzlePath) points, ext = os.path.splitext(filename) points = int(points) - puzzle = Puzzle(open(puzzlePath)) + puzzle = Puzzle.from_stream(open(puzzlePath)) puzzles[points] = puzzle for points in sorted(puzzles): From ee409479efb5af7ea36479f7b35d431f1ba02ee7 Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 17 Oct 2016 15:37:11 -0600 Subject: [PATCH 03/18] Backward compatible with Python 3.4 --- devel-server.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/devel-server.py b/devel-server.py index 410e109..016a5d6 100755 --- a/devel-server.py +++ b/devel-server.py @@ -8,7 +8,14 @@ import pathlib import puzzles import socketserver -HTTPStatus = http.server.HTTPStatus + +#HTTPStatus = http.server.HTTPStatus +if hasattr(http.server, 'HTTPStatus'): + HTTPStatus = http.HTTPStatus +else: + class HTTPStatus: + NOT_FOUND = 404 + OK = 200 def page(title, body): return """ @@ -125,7 +132,7 @@ you are a fool. return None content = mdpage(text) - self.send_response(http.server.HTTPStatus.OK) + self.send_response(HTTPStatus.OK) self.send_header("Content-type", "text/html; encoding=utf-8") self.send_header("Content-Length", len(content)) try: From 7469daa6d1f7911a57bc00cbd0836bdc4c8a289a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 17 Oct 2016 16:10:41 -0600 Subject: [PATCH 04/18] Fix character set issues on Windows --- devel-server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devel-server.py b/devel-server.py index 410e109..6ec4e74 100755 --- a/devel-server.py +++ b/devel-server.py @@ -93,7 +93,7 @@ you are a fool. body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=puzzle)) elif len(parts) == 4: body.append("# {} puzzle {}".format(parts[2], parts[3])) - with open("puzzles/{}/{}.moth".format(parts[2], parts[3])) as f: + with open("puzzles/{}/{}.moth".format(parts[2], parts[3]), encoding="utf-8") as f: p = puzzles.Puzzle(f) body.append("* Author: `{}`".format(p.fields.get("author"))) body.append("* Summary: `{}`".format(p.fields.get("summary"))) @@ -126,7 +126,7 @@ you are a fool. content = mdpage(text) self.send_response(http.server.HTTPStatus.OK) - self.send_header("Content-type", "text/html; encoding=utf-8") + self.send_header("Content-type", "text/html; charset=utf-8") self.send_header("Content-Length", len(content)) try: fs = fspath.stat() From a72c5f81a8f3fe4624042ef9758f269af7fa9f4a Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 17 Oct 2016 17:23:03 -0600 Subject: [PATCH 05/18] Created new credits file. --- CREDITS.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 CREDITS.md diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..6bb89ff --- /dev/null +++ b/CREDITS.md @@ -0,0 +1 @@ +Shannon Steinfadt From dd43df9404a0d740b7c5b7280b557acc8f9c9f54 Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 17 Oct 2016 17:25:45 -0600 Subject: [PATCH 06/18] credit Neale --- CREDITS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CREDITS.md b/CREDITS.md index 6bb89ff..5d74b0b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1 +1,2 @@ +Neale Pickett Shannon Steinfadt From 3c4a340652282c0a465ab76d151450a6419651b8 Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 17 Oct 2016 17:26:53 -0600 Subject: [PATCH 07/18] Add Pat to credits --- CREDITS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CREDITS.md b/CREDITS.md index 5d74b0b..d452e20 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,2 +1,3 @@ Neale Pickett +Patrick Avery Shannon Steinfadt From 2e20af07478786a615da2a1d335495fe4a77e39e Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Mon, 17 Oct 2016 19:58:51 -0600 Subject: [PATCH 08/18] Files now come with a file handle to the original file. --- answer_words.txt | 4096 ++++++++++++++++++++++++++++++++++++++++++++++ devel-server.py | 9 +- puzzles.py | 196 ++- 3 files changed, 4278 insertions(+), 23 deletions(-) create mode 100644 answer_words.txt diff --git a/answer_words.txt b/answer_words.txt new file mode 100644 index 0000000..55d9e06 --- /dev/null +++ b/answer_words.txt @@ -0,0 +1,4096 @@ +abalones +abashed +abattoir +abbreviating +abduct +abetters +abettors +abhorrent +ablution +ablutions +aboriginal +aborted +aboveboard +abrades +absence +absorbents +abstention +abstraction +abuser +academically +acceptably +accepting +accompanists +accumulative +accuracy +accurate +accusers +acetate +acidifying +acidity +acolytes +acoustic +acquires +actualized +acuity +adagios +addicting +adherent +adjacently +adjectivally +adjoined +adjuration +adjustable +administrates +admiringly +admonish +admonishments +adores +adorns +adulterers +advance +advancing +advert +advisory +aerosol +aerospace +affability +affable +afflict +affordable +affront +afterbirths +agates +agglomerates +aggravations +aggressiveness +agnostic +agrarian +aiding +ailments +airfoils +airily +airless +aisles +albatrosses +alcoholism +alderwoman +alerting +aliasing +aligns +allayed +allays +alleyway +allocated +allocates +allocation +allotments +allowances +allures +alluring +allusion +aloofness +alternately +always +amalgam +amalgamations +amanuenses +ambassadorship +ambiguity +ambulance +amenity +amiably +amoeba +amoebae +amorality +amounting +amphitheater +amplified +anaconda +anatomy +ancestors +android +anesthesia +anesthetic +angina +angular +animal +animate +animating +animations +anions +annealing +announcements +annoyingly +annoys +annuals +answering +antagonize +antecedent +anthropomorphic +anticlimaxes +antique +antiwar +apathetically +aphelion +aphrodisiacs +apostrophe +apostrophes +appeases +appellation +appendage +appendages +appetizingly +applaud +applauded +applesauce +appliances +applicators +appointing +appraises +apprise +approbations +arboretums +archaeologists +archeologist +archest +archivists +arcing +ardors +arguing +argyles +armadas +arraigned +arraignment +arrangers +arrowhead +arsenals +artichokes +artificially +artistically +ascensions +ashamed +assault +assaulter +assaults +assertiveness +assign +assignations +assignments +assizes +assumes +assuming +astounds +astral +astrological +atrocity +attacked +attained +attempted +attendances +attentiveness +attitudinizing +auctioneers +auctioning +audiences +audits +augurs +aureole +auspiciously +authorizes +authors +automaton +aviaries +aviator +avionics +avoidable +avoirdupois +avowed +awaiting +awaken +awarded +aweigh +awfuller +babble +babysit +bacchanal +backpackers +backpacking +backpedaling +backstage +badness +bagpipe +balanced +baleen +balloon +ballyhooing +baluster +bamboozles +banishment +bankruptcies +banned +bantam +banters +banyans +baptismal +baptisteries +baptized +barbered +bargains +barnstormed +baroness +barraged +barrelled +barrelling +barricaded +barroom +baseless +baselines +bassists +bathing +batsman +beaching +beacons +beaker +beamed +beaning +beatified +beatitude +beautifier +becoming +bedazzled +bedfellow +bedrocks +bedtime +beetles +befouled +beginnings +begrudge +begrudges +beguiled +beholds +beleaguers +belligerently +bellow +bellwether +bellyached +belong +belting +bemoans +benefit +benign +bequeathed +bequeathing +besieged +besieging +besmirches +bestir +bethinking +betokening +bewilders +bewitched +bicameral +bigamist +biggest +binders +binged +birdied +bitchiest +biting +bittersweets +bivouac +bivouacking +blackball +blacked +blackmailed +blackness +bladder +blamed +blaming +blancmange +blaring +blasphemer +blasphemes +bleakest +blessings +blinked +blistered +blocks +blondest +blooding +bloodshot +blotchier +blower +boarders +boasts +boaters +bobbed +bobbing +bobolink +bobsledding +boggle +boldface +bologna +bolting +bondage +bondsman +bonging +bonitoes +boogie +bookkeeping +bookstore +boomeranged +boondoggled +boosters +boosting +bootee +booths +bootlegged +botanical +botanist +bottomed +bouillabaisse +boulevards +bountiful +bouquet +bracelet +bracts +braining +brandies +brandishes +brashest +brawled +brazenly +breach +breaded +breadfruits +breadths +breadwinner +breakage +breastbones +breaststroke +breezier +breviaries +bricklayers +bridal +bridals +brides +bright +brilliant +brilliantly +briquettes +brisket +bristles +bristly +brittleness +brittlest +broadcasting +broadened +broadest +broadness +broadswords +brochure +brochures +broncos +brontosauruses +brownout +browser +bruises +brunettes +brushed +bucketfuls +buckler +bucktooth +buffalo +buffing +buildings +buildup +bulldozes +bullet +bulletined +bulrushes +buncombe +bunion +buoyed +burglarize +burnoose +burros +burrows +busheling +bushwhacking +busybodies +busying +butcher +butchers +buzzards +cablegram +cables +cadaverous +cadger +calcium +calculated +calligrapher +callused +calmed +calved +camber +camellia +cameramen +camisole +camouflaged +camper +cancan +cancans +cancelled +cancers +candles +cannabis +canneries +cantankerously +canteens +cantered +canters +canticles +capering +capons +capriciously +capsize +capsizing +capstan +captain +captivating +caramel +carcass +cardiogram +caressing +caribou +caricaturing +carnivals +caroler +carousels +carpeting +carport +cartwheel +carves +caseload +caseworker +caseworkers +cassava +cassino +castigated +castigators +castings +catalysts +catcalls +catechized +caterers +catheter +cattails +cattle +catwalks +caudal +caught +cavaliers +caveman +caving +cavorting +ceases +ceasing +celebrant +celebrated +celebrates +celerity +celesta +celestial +cellulars +cellulite +censure +censuring +centenarian +centenaries +centers +centralized +cerebellum +ceremonial +ceremony +cesarian +chaining +chairmanship +championing +channelled +channelling +chantey +chapel +chaplaincy +chapped +charitable +charity +charming +charted +chartered +chases +chastened +chastised +chatters +chattier +chauffeured +cheapskate +checkered +cheekbones +cheerleaders +cheerless +cheesecloth +cheesiest +cheroot +chessboard +chewing +chickpea +chiding +chiefer +chiefest +childhoods +chilliness +chillings +chilly +chisel +chisels +chlorinated +choking +choleric +choruses +chronically +chronology +chrysalises +churchyards +churls +cicadas +cinctures +cinematographer +ciphering +circulation +circulations +circulatory +circumcising +circumflexes +circumscribes +circumventing +citizenship +civics +civilians +civilized +civvies +clamminess +clandestinely +clapboarding +clapboards +clapper +clarinetists +clarity +classify +clauses +cleanings +clematises +clergies +clergymen +clerked +cleverness +climactic +climaxes +clippers +clippings +clitorises +cloakroom +closest +cloudy +clutter +coalesce +coarse +coasted +coasting +coasts +cobble +cobblers +cobbling +cockiest +cocooning +codding +coeducational +coeval +coffeecake +coffin +cognac +cohabiting +coined +colder +coldest +collapsible +collectible +collectibles +collectivize +collects +colleges +collie +collocates +colluded +colonizer +coloraturas +column +combed +combustible +comedies +comforted +comity +commandeered +commandeers +commencement +commiserations +commissioner +commutative +compartmentalizes +compassed +compassing +compel +competition +competitively +compiles +completeness +complexes +complicated +component +composer +composite +composition +compound +compounding +comprehensible +compresses +compromising +comptrollers +computes +conceal +concentrating +concentrically +conceptualizations +conciliates +conciser +conclusively +concussions +condemning +condenser +condescends +condoled +condor +conduce +conducing +conducive +conduction +confabs +confederations +confessions +confidante +confinement +confiscate +conflicted +confronting +congestion +conglomeration +congratulate +congratulations +congress +congresses +congressmen +conjecture +conjoint +conjugated +conjurors +conked +conking +connecter +conquers +consciously +consecrates +consequences +considerately +consigning +consignment +consist +consisted +consolidates +conspicuously +conspired +constituted +constrictions +consummated +consumptive +containing +contaminates +contemplative +contest +contiguous +continuation +contort +controversies +contusions +conurbation +convenes +convenient +convergence +converter +convivial +convolutions +convoys +convulses +cooked +coolly +coopers +coordinator +coppery +coquetted +coquetting +corals +cording +cornea +cornrows +cornstalks +coronet +corralled +correspondent +cosigns +cosmetologists +cosmically +cosmologist +costars +cotter +cotton +cottonmouth +cottons +coughs +councillor +counterpoints +counterrevolutionaries +counting +courtliness +courtly +coverlets +cowardliness +cowhide +cowing +crackup +craftily +crashes +credential +credibility +creditable +creels +creeper +cribbing +crimsoning +crinkles +crinoline +crisscrossed +crisscrosses +crofts +crooners +crosschecked +crowns +cruddiest +crudely +cruelly +cruets +crumpet +crunches +crunchiest +cryogenics +crystal +crystalized +crystallize +cuckoos +cultivated +cultural +culvert +cupping +curiously +curliest +currant +curriculums +curtailed +curtailments +cushions +cussing +cyberpunk +cyclamen +cymbals +cytoplasm +dabble +daemons +daffiest +dahlias +daintiest +damaging +dampest +damply +danker +dankest +dapples +darken +darling +dashes +datelined +daubers +daydreamed +daydreaming +deactivating +deadbolt +deaden +dearness +deathtraps +debater +debaters +debauches +debilitate +debits +debonair +debtor +debtors +decadently +decadents +decays +deceitfully +deceitfulness +deceiving +decelerate +decimate +decked +decoding +decolonize +decomposed +deconstruction +decontaminated +decontaminating +decoration +decreasing +deducing +deductible +deducting +deescalating +defaced +defected +defendants +deferments +deflect +defogger +degraded +dehumidifier +dehydrates +dejecting +delegates +deletes +delinquently +deluges +delusions +delusive +delves +delving +demagog +demagogue +demarcate +demarcated +demeans +demesne +demijohn +demise +demoed +demonstrators +demoralize +demoralized +demurring +demurs +dendrite +denied +denizen +denizens +denominates +denominator +denser +dentist +dentists +deodorizes +dependability +dependant +depict +depleted +depleting +deported +deposing +deprecating +depriving +deputies +derail +deregulates +derive +dermatology +dervish +descants +descriptively +desecrating +desert +desire +desolately +despatches +desperate +destination +desultory +detain +detergent +determiners +dethrones +detoxification +devaluation +development +developments +develops +deviate +devised +devours +devout +devoutest +dhotis +diagnosed +dialogues +diddling +diereses +digestive +digitized +digressed +digressing +dilation +dilatory +dillydallies +dillydally +dimensional +diminished +dimwit +diplomacy +diplomata +dippers +dipsticks +directions +directorship +directory +dirigibles +disabusing +disadvantaging +disallowing +disappointingly +disapprovingly +disarms +disassembles +disbarred +disburse +discern +disclosing +discoing +disconnect +disconnections +discontentedly +discontinuations +discontinues +discording +discotheque +discount +discourages +discoursed +discus +discussions +disembowels +disgruntled +dishcloths +disheartens +dishevelled +dishonoring +disillusioning +disinclined +disliking +disorganize +disparaged +dispassionately +dispenses +displayable +disposes +disproven +disruptive +dissected +disservice +dissociated +distastefully +distastes +distillations +distilleries +distillers +distinguishing +distracted +distributions +distributive +diverging +dividend +diviners +divisibility +divisors +divorcing +dizzies +docile +docility +dockets +doctrine +dogfights +dogies +dolefully +domesticity +domicile +domineered +dominion +donate +dormitory +dorsal +dossiers +doubtless +douches +doughtiest +downhill +downplays +downscale +downsize +dozing +drachmas +draftiest +dragonfly +dramatizing +drawbridges +drawstrings +dreaded +dreadful +dreadlocks +dreamlike +dreamy +dreary +drenches +dressage +dressiest +drifted +drivelled +driven +drizzles +drowse +drowsily +drowsiness +drummed +drumming +drumstick +duelling +dumbfound +dunner +duodena +duplicity +during +duskiest +dustbin +dustman +dwarfism +dwelled +dwindled +dynamite +dynamos +earned +earnestness +earthwork +easily +easterlies +eavesdroppers +economized +editorship +effete +efficacious +effusions +eggplants +elaborate +elapse +elating +elbowed +electoral +elicited +elides +elites +ellipse +elongations +emaciating +emancipates +embalmers +embalms +embarking +embitters +emboldening +embossed +embrace +embryos +eminently +emoted +emotions +emphases +emphatic +emphatically +empire +empires +employment +emulsifies +emulsify +enamor +encapsulate +encapsulation +encase +encoded +encoder +encompass +encouragements +encrusted +endeavor +endures +energies +enfeebled +enfolding +enforcers +engages +engender +engenders +engraver +engulfing +enlargement +ennobles +enquiring +enquiry +ensconcing +enshrines +ensnares +entail +entails +entangling +entertainment +enthralls +enthusiastically +enticed +enticements +entire +entombed +entraps +entrusted +enumeration +enunciates +enunciation +envelops +envisions +epaulettes +ephemeral +epidemiology +epilogues +epoxied +equality +equalizing +equestrian +equilateral +equivocates +erasing +erection +eroticism +errors +eruptions +escapees +escorting +espouses +essaying +essayist +essentials +established +establishes +estates +ethically +ethics +ethnologists +eulogizes +euthanasia +evaporating +evened +evener +everlasting +evocation +evolves +exactest +exceed +excepting +excepts +excerpt +excessively +excitement +exclaims +exclusives +excoriate +excretions +exculpate +excuses +executable +executors +exhausted +exhilarated +exhumations +exigent +exiles +existent +existentially +existing +exoduses +exonerating +expanding +expects +expediences +expedites +expending +experiments +expiry +explanatory +exploring +expressed +expressing +expressiveness +expressly +expunge +extemporized +extension +extinguished +extrapolates +extrapolation +extricates +extroversion +extrovert +exults +eyeful +eyelets +facsimiled +factory +factual +faecal +fagged +fainted +fairness +faithfuls +faithlessness +fallibility +fallowed +falsest +falterings +famines +famished +fanatical +fancying +fantasy +farmhands +farmland +farsighted +farting +fastenings +fathoms +feasibility +feasting +featherweights +felicity +feline +fermentation +ferrets +fertilization +fertilizer +fervidly +festoons +fetiches +fetter +feuding +feverish +fibber +fiendish +fiercely +fifteens +figurehead +figures +filching +fillies +filliped +filtering +finalizing +finders +finessed +finesses +fingerprint +finishing +finking +firestorm +firmer +fishbowl +fishier +fishiest +fishnets +fishtailing +fisticuffs +fitted +flagellate +flakiness +flamencos +flames +flanneling +flapjacks +flashier +flatfoot +flatness +fleecing +flicks +flimsiness +flings +flintlocks +flippantly +florid +flotillas +flounces +flourish +fluffing +fluoroscope +fluxing +flyleaves +flyovers +foetuses +fogbound +fogging +foghorns +follow +fondles +fondue +footballers +footbridges +footstep +forborne +forebears +foreboding +forehand +foreshortened +foreskin +forestalls +foresting +foreswore +foretasting +foreword +forklifts +forsworn +fortifies +fortissimo +fortitude +fortuitously +forward +fossil +fossilizes +foundations +foxtrotting +fracas +fractiously +fractures +frailest +frailty +frantic +frappes +fraternally +fraternization +fraudulently +freebooters +freewheeled +freewill +freighters +frenetically +frequenting +freshman +freshmen +friendship +friers +fringes +frisking +frittering +frizzed +frolicked +frontispiece +froths +fruitiest +frustrates +fulminations +functioning +fundamentally +funerals +furling +furloughing +furnish +furnished +furors +furriest +furrows +furtherance +fustier +gabardine +gadded +gaffed +gaggle +gainsays +galena +gallantly +gallery +gallivanted +gallivanting +gallstones +gamesmanship +gamest +gangrenes +garbanzo +gardening +garishly +garners +garnish +garnished +gashes +gather +gauchos +gaudiest +gearshifts +gearwheels +geishas +gelling +geneses +genetic +genetically +geneticist +genteel +gentian +gentlefolk +genuineness +geodesics +geostationary +gesticulated +gesture +gestures +gherkin +ghostwriting +gibberish +gibbon +gibing +giblet +giddily +gigabyte +giggles +gigolo +girdled +glamored +glamour +glassier +glazing +gleaned +glimmering +glimpse +glistened +globetrotters +globule +glories +glossary +glower +gluttonous +glycerine +gnashed +goalies +gobbler +godsends +goldbricking +golfed +gondoliers +gonged +gonorrhea +goslings +gossamer +gouges +goulash +governance +gowning +gracefully +grandee +grannies +grapnel +grateful +gratified +gratis +graying +greases +gregarious +grenadiers +greyhounds +grilled +grislier +gristlier +grossest +grottos +grouses +gruelings +grunted +guardrail +guessed +guesstimate +guidance +guided +guileful +gulled +gulling +gummed +gunboat +gunpowder +gushiest +gutters +guzzling +gymnastic +gynecology +haberdashery +habituates +hackneying +hackneys +haemophilia +haggled +hairbreadth +hairless +hairpiece +halberd +halftimes +hallelujahs +hallmarking +hamlets +hammed +hammered +hammers +handbags +handballs +handcraft +handcuffing +handedness +handicapper +handily +handing +handkerchiefs +handlebars +handling +handpicking +handshake +handstand +hangars +hanged +hangings +hankerings +harassing +hardcover +hardiest +harelip +harems +harmfully +harmonizes +harrowed +harvest +hashed +hastened +hatchbacks +haversacks +hawing +hayseed +headers +headland +headsets +headwinds +healthfully +hearkened +hearses +heartened +heartens +hearth +heaters +heaven +heavens +heckler +hedgerows +hedonistic +heehawed +heiress +heliotropes +helium +hellebore +hellion +helpmates +heppest +herbicide +hereditary +heretical +heroins +heroism +hibernating +hierarchical +hierarchy +highballs +highbrows +highjack +highjackers +hindquarter +hindrance +hippie +hirsute +historians +hitched +hitchhike +hitchhiker +hitchhikers +hitting +hoagie +hoarded +hobbling +hobgoblin +hobnailing +hockey +holdout +holdouts +holdover +hollowing +holography +homeland +homeopathy +homesickness +homogeneity +homogeneously +homogenization +homosexuals +honest +honeymooners +honorariums +hoodwink +hoofing +hoorayed +horticulturist +hosannas +hoteliers +householder +housemaid +housewares +hubcap +huffily +humanizer +humerus +humiliate +hungriest +hurrying +hurtle +hussies +hutches +hutzpa +hydraulic +hydrogenates +hydrology +hydroplane +hydroplanes +hyenas +hygienic +hyperventilated +hyphenations +hyphened +hypochondriac +hypochondriacs +hypoglycemia +hypothesize +hysterectomy +hysterical +hysterically +idealist +identically +identifying +idolatry +igniting +ignorant +illumined +illusive +illustrated +illustrator +imagining +imbalance +imbedded +imbibes +imitates +immediacy +immigration +immortally +impaired +impairment +impaneling +impanels +impediment +imperatively +imperiling +imperils +imperishable +impertinence +impinged +impingement +implacability +implant +imposingly +impound +imprimaturs +imprisonments +improperly +imputed +inaccessibility +inaccurately +inaction +inadequacy +inamorata +inaugural +inbred +inbreeds +incarcerations +incinerators +incisiveness +incite +incomes +inconstancy +incredulously +incriminate +incrusted +incurably +indecipherable +indentations +indestructible +indicatives +indicator +indictment +indigestion +indignant +indiscreetly +indisposition +indorsed +inducing +inductions +industrial +industrialism +inebriated +ineffectiveness +ineptitude +inessential +inessentials +inexorably +inextricable +infamous +inferior +infertility +inflames +inflatable +inflate +infomercial +informed +infringe +ingraining +ingratiatingly +inhabit +inhalation +inhalations +inheritors +initiates +injection +inkblots +inmates +inoperative +inputting +inquest +insecurities +insensitivity +insentient +instability +instances +instills +institutionalize +institutionalized +instrumentalist +insularity +insureds +intake +integrates +integrating +intelligence +intend +intensifiers +intercepted +interface +interfered +interjects +interleaved +interluding +internal +internalize +internationalism +interpret +interrogation +intersection +interviewees +interweaving +intriguingly +introduce +introverted +intrudes +inundate +inveighs +inveigle +inventive +invents +investment +invests +invitational +invoice +invoke +ionizing +ipecacs +irking +irrefutable +irregardless +irregular +irrelevancy +irresolute +irrespective +irretrievably +islands +isolated +isolating +italicizing +itemized +iterator +itinerary +jabbered +jackrabbits +jading +jamboree +jangle +jauntily +jaunting +jawbones +jazzier +jazziest +jeopardizes +jerkiest +jitney +jokers +jonquil +joshing +jounced +jounces +journalism +jovial +joyfully +joyriders +judgmental +juggler +juicers +juicing +junkie +junkier +justest +justify +juxtaposing +kamikaze +kangaroos +katydid +kayaking +keening +kenneling +kerosine +kettledrum +khakis +kibitzers +kickbacks +kicked +kickers +kickstand +kickstands +kielbasas +killdeers +killers +kilobyte +kindliness +kinematics +kisses +kitchen +kleptomania +kneads +knitters +knitwear +knobbier +knotted +knuckle +kookaburras +kopecks +laboratories +ladles +lallygagging +lamentably +lamming +lancers +landladies +landowner +languages +languishes +lapsed +laryngitis +lassies +lateraling +latterly +laudable +launderer +laundering +lavishest +lawless +lazies +league +learned +leased +leases +leaven +lectern +lecturing +leeches +leerier +leeriest +legatos +leggins +legislator +legislatures +leonine +lethally +letters +levitates +lexicographer +liability +liable +libeling +liberalization +liberally +libertine +librettos +licensed +licentiate +lieutenancy +lifespan +lightning +likeliest +likenesses +limelight +limousines +limpets +linage +linearly +linefeed +lingeringly +liniments +linnets +lionized +lisped +literals +livens +livestock +loader +loading +lobbed +lobbying +localizes +locket +locking +locust +lodges +logarithm +logbook +loggers +logistics +lollygags +longed +longingly +looked +looming +loonies +lordly +loudspeaker +loveable +loveliness +lowers +loyalty +lubricator +lucidness +lugubriously +lumberjacks +luminously +lunched +luncheon +lunges +luring +lusted +luxuriantly +luxury +lyceum +macing +macintoshes +macroscopic +mademoiselle +madras +madrigals +maggots +magnanimous +magnesia +magnetize +magnified +magnolia +magnolias +maharaja +maharajas +maharanee +maiming +majestic +making +maladjusted +malfunctions +mallows +manhandled +maniacal +manicured +manning +mantlepieces +marcher +margin +marginally +marigold +marihuana +marinade +marketed +marketer +marmalade +marquess +marquis +martini +martinis +marveling +marvels +masculines +mashed +masochism +masochists +massiveness +material +materialism +matures +maturities +matzot +maximal +mayors +mealier +mealtime +mealtimes +meandered +meanly +measurably +meatballs +mediaeval +mediation +meeting +mellower +meltdowns +member +members +membrane +menaces +menagerie +menopause +menorahs +meretricious +merganser +merged +merges +mermaids +merrymakers +metallurgical +metatarsals +meteorologist +methadon +method +mewing +miasma +microwaving +midmost +midterm +midyears +miffing +mightiest +migraines +migration +milepost +militia +milkman +milkshake +mimosas +mincing +mingled +minibike +minicomputers +minimalists +mining +minstrel +minstrels +misadventures +misanthropes +misapply +misconceive +misconstrue +miscounted +miscued +misdealt +misdiagnosis +misdoings +misfires +misfitted +misguiding +mishap +misidentified +misinterpretation +mislays +misogynist +misogynists +misprints +misrepresent +misrepresentations +missal +mistaken +mistiness +mistreating +misuses +mixture +mizzen +moccasin +mockingly +modernism +modernity +modernize +modulates +moisturizes +molder +moldier +molecular +molluscs +mollycoddles +mommas +monetary +moneymakers +mongoose +monicker +moniker +monkeyed +monkeys +monoliths +monolog +monopolizes +monosyllabic +monosyllables +monotony +moochers +mooned +moonshine +moonshines +moralizes +moratoriums +mordants +mortgagees +morticing +mortifying +moseys +mosques +mothballed +mothering +motherland +motivate +motivates +motivating +motocross +motorcar +motorists +motorized +motormouth +mountaineer +mountainside +mountebanks +mounted +mourned +mournful +mousses +mouthing +moving +muckiest +mucking +mudslingers +muezzin +muffin +muftis +muggers +muggiest +mulligatawny +multiplexors +multiplication +multiprocessing +multitude +multivariate +mumblers +munching +muralist +murders +murkiest +mushiness +mushing +musicology +muskmelon +mustered +mutability +muteness +muting +muzzled +myrtles +napalmed +narcissuses +narrating +nasalizing +national +nattily +naturalize +naturalizes +naturally +natures +navigate +necessaries +necessity +neckerchiefs +necklace +needles +nefarious +negligently +negligs +neighboring +nephew +nerves +neurotic +neuter +newscasters +newspapers +newsreel +nicknames +nicknaming +niftier +nightclubbing +nightingale +nightmare +nightshirt +nimblest +ninety +nobility +nobleness +nobler +noiselessly +noising +nominations +nonagenarians +nonempty +nonevents +nonexistence +nonobjective +nonpayment +nonrenewable +nonwhites +normalize +normalizing +northeast +northeastern +northeastward +notwithstanding +nuggets +nullify +numerating +numeric +nursed +nutmeats +nutted +objection +obscene +obscurest +observable +observing +obsessive +obstacle +obstruct +obtrudes +occlude +occluded +occluding +occupational +occurrences +oceanographic +oddity +odious +odometers +offenders +offends +offensiveness +offices +officials +officiate +offing +offsetting +offshoots +oilfields +oleanders +omelette +omnibus +omnivores +onlooker +operetta +oppresses +orbital +ordaining +orderliness +orientating +ornaments +ornate +ornerier +ornithologists +orphaning +orthodontics +orthogonal +oscillating +oscillators +osteoporosis +ounces +ousted +outbidding +outdistance +outfitter +outgoes +outlays +outnumbered +outnumbers +outplacement +outputted +outrageous +outriders +outselling +outshining +outsiders +outstretched +outvotes +outwitted +overacts +overate +overawes +overbearing +overcharged +overcome +overcompensation +overdose +overdoses +overexposed +overexposes +overexposing +overexposure +overextending +overheating +overpays +overpower +overpowered +overseer +oversees +overshoe +oversimplifications +overstay +overtaxes +overture +overweening +overwhelmingly +overworking +ownership +pachyderm +pacifier +packet +padding +padres +pageant +paintbrush +paintwork +pajamas +paleness +palest +palindromes +palladium +pallbearers +pallets +palmiest +palsying +pancake +pancreatic +pandemics +pandering +panelled +panoplies +panoply +pantheons +panthers +pantomimed +pantsuits +papergirls +parable +parachuting +paradoxically +paragraphing +parakeets +parallelisms +paralytic +parameter +parameters +paramilitary +paranoid +paranormal +paraphrasing +paraplegia +parboiling +pardoning +parented +parked +parkway +parlays +parquetry +parries +parrying +partake +particularity +particularization +particularizes +particularizing +partisanship +partnered +partook +passageway +passageways +passel +password +pasteurization +patents +paternal +patinae +patriarchy +patricides +patriot +patriotically +patronize +patronizingly +paunches +peaceably +peccaries +peccary +pectorals +peculiarly +pedagogical +peddler +peddlers +pedlars +peerless +pellets +pendulous +peninsula +penlites +pennies +pensioner +pensioning +pentagon +penthouse +penury +peppercorn +peppermint +pepperonis +peppier +perceivable +peregrinations +perennially +perfecter +perfectionists +perforation +performer +perfunctory +perilous +periodical +periodicals +peripatetic +peritoneum +perking +peroxided +perpetuate +perplexities +perseveres +personage +personify +persons +persuasively +perter +pervaded +petiole +pettifogged +petunia +petunias +phalanx +phalluses +pharmacopeia +philanderer +philter +phoebe +phonetics +phonic +photocopied +photographic +photography +phraseology +physicked +piazze +piccalilli +pickaxes +picketing +pickled +piddling +piffle +piggish +piggishness +piggyback +pilferer +pilling +pillory +pinnacle +pinochle +pinprick +pintos +pipped +piquing +pirouetted +pisses +pistachio +pistil +pistils +pistons +pitched +piteous +pitiable +pitiless +pitons +pivotal +pixels +pizzerias +placeholder +placentas +placer +placidly +plagiarism +plagiarizing +plague +plainclothesman +plainest +plaintiff +plaits +playgoer +playhouse +playpen +pleads +pleasing +pleasure +plenipotentiaries +plenitudes +plexus +pliancy +plinths +plodding +plowman +plowshare +pluming +plunderers +plunking +plural +plutocracies +plutocratic +plutocrats +plutonium +plying +pockmarked +poetically +poised +pokeys +police +poliomyelitis +politeness +pollinate +polliwog +pollute +pollywogs +polyhedra +polythene +pommel +poniards +pooched +poodle +portables +portage +portaged +portent +portly +portmanteaus +portray +portrays +positioned +positioning +possession +possessions +possessively +postdoc +postmarking +postmodern +postpaid +postscript +posture +postures +potboilers +potholders +potpies +potshots +pouches +poultices +powered +powerhouse +powwow +powwowing +practicability +practicably +practical +pranced +pranksters +preachier +precluded +precludes +preconceived +preconceives +predicates +predication +preexists +preferable +prefixed +prejudices +preliminary +preludes +prematurely +premeditating +premiere +premising +preordaining +prepackage +prepositions +prepossessed +prepossessing +preppie +prequels +presage +presaged +presages +prescience +presences +presentations +presents +presidents +pressing +presume +presupposes +preterit +preterits +prettified +prettifies +prevalence +prevaricated +prevaricating +preventing +prided +primly +primmer +printer +prisms +privater +privatization +privatized +privileges +prizefighting +proclivities +proclivity +procrastination +procreates +proctoring +procured +procuring +prodding +producing +professorships +profiling +profiteered +profiteering +profusely +profusions +programing +programmer +programmers +progression +prohibitory +projectile +proliferating +prolongations +promiscuous +promptest +prompts +pronouncements +proofs +propagandists +propagandized +propellent +propeller +propelling +propensity +properly +prophesied +prophylactics +propounds +prorates +proscriptions +proselytes +proselytizing +prosperous +protectiveness +protruding +protuberances +providence +provisional +provocatively +psyched +psychobabble +psychologists +psychopaths +psychotics +ptomaine +publicity +puddled +puffins +pullout +pulpits +pulsars +pulsate +pumice +pumpkin +pupils +pureed +purism +puritanical +purplest +pyromaniacs +quadraphonic +quadruplicating +quahogs +qualification +queenlier +queerer +questionable +questioned +questions +quicker +quicklime +quiescence +quietus +quilted +quintessences +quintuple +quirking +quitter +rabbited +raccoon +racial +racism +racketeered +radiotelephone +radium +raffles +raggedest +raggediest +ragtags +raining +rainmakers +raised +ramrodded +rancid +rancorous +ransom +ranting +rapport +rasher +raspberry +rations +rattlesnake +rattletraps +rattlings +razors +reachable +reaches +reactions +readied +reaffirms +reappoints +reason +reasons +reassemble +reassembles +reassured +rebind +rebuke +rebukes +recapitulating +recede +receipt +receives +recently +recess +recessions +recheck +rechecks +recipe +reciprocating +recoil +recoils +recompensed +recompenses +reconcile +reconciles +reconsidering +reconvening +recorder +recording +recount +recovering +recriminated +recriminating +recruiters +recruitment +recursion +redcoat +reddened +redeemed +redoes +redouble +redoubles +redounds +redrawn +redraws +reduction +redwood +reeducation +reefing +reelect +reenforced +reenlist +reexamine +refines +refocussed +refrigerates +refrigerating +refuelled +refurbish +refuse +refutation +regales +regency +regenerates +regimenting +regularized +rehabbed +rehashed +rehashes +rehired +reimburses +reimposed +reiterating +relabelling +relapsed +release +relentlessness +reliance +relocation +reluctant +remade +remand +remarks +remedied +remedies +remiss +remodel +remodels +remotely +remounting +removal +remunerating +renders +rendition +renegade +renegaded +renews +rental +rented +reoccupying +repairable +repeal +repeatedly +repellants +repellents +rephrasing +replacing +replaying +replete +replying +reposes +reprehends +represent +representation +repressed +reprieve +reprieving +reprise +reprises +reproofing +reptiles +repulsing +repulsiveness +requested +requirement +reread +resale +reschedule +reschedules +rescission +resentfully +resents +reserves +reside +residents +residue +resigns +resinous +resolves +resonates +restatement +restaurateur +restaurateurs +resulted +resumed +resurrecting +resuscitate +retards +retirements +retrain +retread +retried +retrievable +retrieving +retrogression +retrorocket +retrospectives +returns +revels +revere +reverencing +reversals +reversing +reviling +revise +revisiting +revivify +revocable +revolt +revolutionize +reworded +rewording +rework +rheumatic +ricocheting +riders +ridiculous +rifling +rightful +rigidly +rigors +riposted +ripping +ripsaw +ritzier +rivaling +rivalling +rivers +roadblocked +roadkill +rocking +roguery +rolled +romanticizes +rooter +rootless +rosier +rostra +rotates +rotational +rotundas +rotundity +roughhouse +roughing +roughnecked +roughs +roughshod +rounder +rouses +roustabouts +routed +rowdier +rowdies +rowdiest +royalty +rubber +rubbished +rubbishing +rubbishy +rubicund +rubier +rudimentary +ruggeder +ruggedest +rumbas +rumors +rundowns +rushes +russet +rustics +rustier +rustiest +rusting +rustle +rustproofs +saboteur +sachems +sacked +sacrilege +sadistically +saffrons +sailors +salads +salaries +salesmanship +salesmen +salines +salmonella +saloons +saltcellar +saltines +saluting +salvage +sanctimonious +sanctions +sandblaster +sandstone +sanserif +sarcastically +sartorial +sassafras +satinwood +satirizing +satisfies +sauted +savoriest +sawing +scalloped +scalpel +scalpers +scandalously +scanners +scapegoated +scapulae +scarecrow +scariest +scarify +scatted +scatterbrain +scavengers +schemes +scheming +scherzos +schoolbook +schoolboy +schoolmate +schoolroom +schoolrooms +scolloping +scorch +scorcher +scoreless +scoutmaster +scowls +scrabbled +scraggly +scrappiest +screwdrivers +screwy +scrimps +scrimshaw +scrimshawing +scrimshaws +scrolled +scrounging +scrupulously +scrutinized +seabeds +seaboard +seaman +seaming +seamless +seamstress +searching +seascapes +seasonally +seaweed +seaworthy +seclude +secluded +secularization +seductively +seedier +seediness +seeped +seesawed +segment +segmenting +seizure +semaphored +semiautomatic +semifinalists +semifinals +seminar +senses +sensibly +sensitize +sensually +sepals +separation +seraphic +serial +serialize +seriousness +serpent +served +service +serviceman +servos +sessions +settle +sevenths +seventieths +severely +sexagenarian +sexiest +shackle +shadowbox +shadowboxed +shakes +shakeups +shallowest +shallows +shambling +shammies +shards +sharking +shaven +shears +sheathe +shebangs +sheered +sheerest +shellac +sheltering +shelved +sheriffs +shillelagh +shilling +shimmy +shingled +shinier +shinning +shirking +shirks +shirttails +shittier +shittiest +shivers +shleps +shocks +shoddier +shoddily +shoehorned +shoehorning +shoeing +shoelace +shoeshine +shooters +shooting +shoplifter +shopped +shored +shoring +shortsightedness +should +shovelfuls +shovelled +showboated +shrewish +shucked +shuffles +shutterbug +shuttlecock +shyster +sickly +sideshows +siestas +signal +signifying +signposts +silencer +silences +silent +silliness +silverfishes +silvery +simplification +sinful +singletons +sinking +sirocco +sitars +sixteenth +sizable +skewering +skimpy +skirts +skylines +slaloming +slammed +slashed +slaughtering +slavish +sleazy +sledge +sledgehammer +sledges +sleepyhead +sleeted +sleeve +slenderest +slenderizes +slicer +slimmed +slinkiest +slipcover +sluing +slumbering +slumping +slurring +smacking +smallest +smelly +smidgen +smites +smoggiest +smokiest +smuttier +snickers +snider +sniped +snipes +snivelled +snored +snorers +snowballed +snowdrift +snowdrop +snowflake +snowmobile +snuffed +soaping +soapstone +sobered +sociability +socialite +sociologist +sociology +softeners +sojourn +soldierly +solely +solemn +solicitor +solicitous +solider +soliloquies +solitaire +somewhere +sophisticates +sophisticating +sorceress +sordidly +sorrow +soulfully +sourer +southbound +southwestern +spacesuits +spadeful +spades +spandex +spanking +speaking +specialization +spectator +spectroscopic +speculated +speculating +spellbinder +spelled +spellings +spendthrift +spermicides +spiking +spills +spillways +spinach +spindlier +spinier +spiniest +spinster +splashdowns +spleen +splice +splines +spoiled +spoilt +spokespeople +spokesperson +sponges +spooked +spoonfuls +sportscast +sportsmanship +spotter +sprawls +sprayed +sprightly +spring +sprinting +sprucer +sprucest +spryer +spryness +spurring +spurted +spurts +sputters +squash +squashy +squeezers +squint +stabilizer +stabled +stables +stagings +staircase +staircases +stalkings +stallions +stamping +stanch +stands +staphylococcus +stapler +starchiest +stared +starlings +stated +statehood +stateroom +statesmanlike +statue +statute +stauncher +staunchly +stayed +steeples +stemming +stencil +steppingstones +stepsisters +sterilized +sterner +stewarded +stewardess +stickpins +stiffen +stiffeners +stilettoes +stilettos +stilled +stimulated +stinkers +stockier +stodgiest +stomach +stomping +stoneware +stoniest +stooping +stoppage +storehouses +storey +straddled +straightforward +straightforwardly +straitened +strange +strangers +strangleholds +strapping +stratagems +strategic +strategist +streakiest +streetlights +strenuous +strews +stridden +strikeout +stringiest +stripe +strongboxes +structure +strudel +struggled +studies +sturdiest +stutterer +stylist +subbasement +subclass +subcompact +subdivided +subjugate +subjunctives +subleasing +submarines +submissive +subornation +subplot +subpoena +subscribed +substations +subsumed +subterranean +subtitled +subtle +subtractions +suburbanites +suburbans +suburbs +successions +succinct +suffering +suffixed +suffocate +suffocation +suffragettes +suicides +suitcases +suites +sulphured +sultrier +summations +sunglasses +sunroof +suntan +superabundances +superber +supercharged +supercharger +superhighways +superpowers +superscripts +supervision +supplant +suppositories +suppository +surfaced +surfacing +surges +surgical +surmised +surmises +surmounting +surplice +surprises +surrendered +surrogate +surround +survived +suspends +suspense +suspension +suspensions +swains +swapped +swarming +swarthiest +swashes +swatted +sweatshop +swellings +swelter +swelters +swerve +swiftest +swinger +switchbacks +switchboards +swords +sycophant +syllabify +syllabus +symbioses +symbolically +symmetrical +symptoms +synagogs +synched +syndromes +synopses +synthesizer +synthetic +syphilis +syphoning +tabloids +tackles +tackling +tadpoles +tailspin +tallness +tameable +tangential +tangos +tanned +tapioca +tariffs +tarnish +tarots +tarpons +tastelessly +tatters +tattles +tattletales +tattoo +taxonomic +taxying +teabag +technicality +technological +technology +teleconference +telegraphy +telemetries +telepathically +telepathy +telephony +telephoto +telethon +televise +televised +telexes +temperas +tempestuous +template +templates +temporary +temporized +temporizes +tempted +tenable +tenacious +tenancies +tenancy +tenoning +tenser +tensing +tentacles +tenuous +terminally +termly +terrorizes +testier +testiness +tetrahedrons +textbook +thankless +theatrical +theologies +theorems +theoretic +therapists +thereabouts +therein +therewith +thesis +thickened +thimbleful +thinker +thinning +thirtieth +thistle +thoraces +thorny +thoughtful +thoughtfully +thraldom +thread +threes +throes +throne +thronging +throttles +thunder +thwarted +tiddlywinks +tidying +tieing +tightened +tightropes +timbering +timekeeper +timetable +timetabled +timing +tinkle +tinselled +tinsmiths +tipper +tippler +tireless +tiring +titillating +titled +toadstool +toasts +toddler +toddling +toffees +toileted +tollgate +tomahawked +tomatoes +tombstone +topics +topographies +topped +topsides +torpedoes +totter +tougher +toughly +towheaded +townsmen +towpaths +toxemia +tracheas +tracing +tractors +trademarking +tradesmen +trammed +trampling +trampoline +trampolines +tranquilizer +tranquillizing +transaction +transcript +transferable +transfers +transformed +translation +translators +transpired +transponder +trapezoids +trappable +treacheries +treatise +treaty +trekking +trembles +tremulous +trenches +trepidation +triage +tribalism +tribunals +trickery +trickling +trimming +trinity +trisect +triter +trivial +trochee +troubadours +troublemaker +trounced +troupes +trouser +troweled +truancy +truffle +trusting +tuberculous +tuberous +tubular +tufted +tufting +tumbrils +tunnies +turnabout +turnpikes +turquoises +turtledoves +turtlenecks +tussle +tussocks +tutorial +twaddles +tweaking +tweediest +twelfth +twenties +twerps +twigging +twisted +twittered +twitting +typecasts +tyrannical +tyrannosauruses +tzarina +ubiquity +ugliness +umbilici +umbilicus +umpiring +umpteen +unanimous +unaware +unbeliever +unbend +unbolts +unbounded +unbreakable +unbuckle +unburden +unbutton +uncensored +uncharacteristic +uncleaner +unclothe +unclothed +unconvincingly +uncooperative +uncork +uncouth +uncover +uncritical +undefined +underachieved +underachieving +underacts +undercurrents +underdogs +underexposes +underfeed +underlines +underlining +underpay +undersecretary +underside +understandable +undulated +uneasily +uneconomic +unenthusiastic +unexpected +unexpectedly +unfetter +unfilled +unfurled +unfurling +ungainlier +unguents +unhealthier +unhelpful +unhinge +unified +uniforming +uninitialized +uninstaller +unjustly +unlatched +unlatching +unleashes +unleashing +unmake +unmasked +unmentionables +unmodified +unobserved +unopposed +unpaid +unpleasantness +unpredictability +unprepared +unprompted +unprotected +unprovoked +unquenchable +unreadier +unreasonableness +unrecognizable +unreliability +unremarkable +unrewarding +unruliness +unscrambling +unseeing +unshaven +unsightliness +unskilled +unsnapping +unsnaps +unsubscribing +unsuccessful +untaught +untiringly +unused +unveil +upfront +upholstered +uprising +uproariously +uranium +urbaner +urbanized +urbanizing +uselessness +usurpation +usurpers +uterine +utters +uvulars +vacuity +vacuums +vagrant +vainglorious +valiantly +validates +valleys +vantages +vaporizing +variations +vatting +vaulter +vaulters +vaunts +vegetarianism +vendor +veneer +venial +ventured +veracity +verbalize +verbalized +verbalizing +verily +vermin +vexatious +victimizing +videodisc +viewed +viewings +vigilante +vilified +villainous +villein +vintages +violate +virtually +visaed +visage +viscountess +visioning +visual +vitiating +viticulture +vocalization +vocations +vocative +vociferates +voiding +voracious +vouchsafing +waiver +waiving +walked +wallboard +wallflowers +walrus +wampum +warbled +warehousing +warmly +warned +warping +warrants +warrens +warthogs +washable +washbowl +washcloths +wasters +wasting +watchfulness +waterfall +wattled +waveform +waxwings +weakens +weaned +webbed +wedding +weeder +weediest +weeper +weepiest +weighed +weirdness +welders +wellsprings +westerners +wetted +wheelbarrows +whetted +whiled +whiling +whimsical +whiniest +whining +whinnying +whipping +whirligig +whirlwinds +whispers +whistling +whitecaps +whitening +whoever +wholesalers +whorls +wickers +wickets +widths +wifelier +wiggles +wildfowl +wiling +willfully +windjammer +windowsills +windscreen +winged +wingless +wingtip +winterier +wiseacres +wisecrack +wisecracking +wishes +wispier +wispiest +withering +withers +withstanding +wizards +wobbled +woefuller +wolfhound +wondrous +wonted +woodier +woodpile +woofer +woofing +woolen +woolies +workings +workmanlike +wormier +worryings +worshipers +worshipping +worthy +wounded +wreathes +wrenched +wrested +wrestler +wrinkling +writer +wrongheadedly +yachted +yammer +yammered +yawing +yearlings +yelled +yessing +yipping +yodelers +yummiest +zealously +zephyr +zeroing +zestfully +zigzags +zircons +zodiac +zoological diff --git a/devel-server.py b/devel-server.py index 732a87b..e280ffb 100755 --- a/devel-server.py +++ b/devel-server.py @@ -8,9 +8,9 @@ import pathlib import puzzles import socketserver -if hasattr(http.server, 'HTTPStatus'): - HTTPStatus = http.HTTPStatus -else: +try: + from http.server import HTTPStatus +except ImportError: class HTTPStatus: NOT_FOUND = 404 OK = 200 @@ -30,6 +30,7 @@ def page(title, body): """.format(title, body) + def mdpage(body): try: title, _ = body.split('\n', 1) @@ -43,6 +44,7 @@ def mdpage(body): class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): pass + class MothHandler(http.server.CGIHTTPRequestHandler): def do_GET(self): if self.path == "/": @@ -142,6 +144,7 @@ you are a fool. self.end_headers() self.wfile.write(content.encode('utf-8')) + def run(address=('', 8080)): httpd = ThreadingServer(address, MothHandler) print("=== Listening on http://{}:{}/".format(address[0] or "localhost", address[1])) diff --git a/puzzles.py b/puzzles.py index e844a00..5b057d6 100644 --- a/puzzles.py +++ b/puzzles.py @@ -1,37 +1,105 @@ #!/usr/bin/python3 import argparse -import base64 +from collections import defaultdict, namedtuple import glob -import hmac -import json +import hashlib +from importlib.machinery import SourceFileLoader import mistune -import multidict import os import random +import tempfile messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + def djb2hash(buf): h = 5381 for c in buf: h = ((h * 33) + c) & 0xffffffff return h -class Puzzle(multidict.MultiDict): +# We use a named tuple rather than a full class, because any random name generation has +# to be done with Puzzle's random number generator, and it's cleaner to not pass that around. +PuzzleFile = namedtuple('PuzzleFile', ['path', 'handle', 'name', 'visible']) - def __init__(self, seed): +class Puzzle: + + KNOWN_KEYS = [ + 'file', + 'resource', + 'temp_file', + 'answer', + 'points', + 'author', + 'summary' + ] + REQUIRED_KEYS = [ + 'author', + 'answer', + 'points' + ] + SINGULAR_KEYS = [ + 'points' + ] + + # 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'))] + + def __init__(self, path, category_seed): super().__init__() + self._dict = defaultdict(lambda: []) + if os.path.isdir(path): + self._puzzle_dir = path + else: + self._puzzle_dir = None self.message = bytes(random.choice(messageChars) for i in range(20)) self.body = '' - self.rand = random.Random(seed) + if not os.path.exists(path): + raise ValueError("No puzzle at path: {]".format(path)) + elif os.path.isfile(path): + try: + # Expected format is path/.moth + self['points'] = int(os.path.split(path)[-1].split('.')[0]) + except (IndexError, ValueError): + raise ValueError("Invalid puzzle config. " + "Expected something like .moth") - @classmethod - def from_stream(cls, stream): - pzl = cls(None) + stream = open(path) + self._read_config(stream) + elif os.path.isdir(path): + try: + # Expected format is path/.moth + self['points'] = int(os.path.split(path)[-1]) + except (IndexError, ValueError): + raise ValueError("Invalid puzzle config. Expected an integer point value for a " + "directory name.") + files = os.listdir(path) + + if 'config.moth' in files: + self._read_config(open(os.path.join(path, 'config.moth'))) + + if 'make.py' in files: + # Good Lord this is dangerous as fuck. + loader = SourceFileLoader('puzzle_mod', os.path.join(path, 'make.py')) + puzzle_mod = loader.load_module() + if hasattr(puzzle_mod, 'make'): + puzzle_mod.make(self) + else: + raise ValueError("Unacceptable file type for puzzle at {}".format(path)) + + self._seed = hashlib.sha1(category_seed + bytes(self['points'])).digest() + self.rand = random.Random(self._seed) + + # Set our 'files' as a dict, since we want register them uniquely by name. + self['files'] = dict() + + def _read_config(self, stream): + """Read a configuration file (ISO 2822)""" body = [] header = True for line in stream: @@ -43,31 +111,119 @@ class Puzzle(multidict.MultiDict): key, val = line.split(':', 1) key = key.lower() val = val.strip() - pzl.add(key, val) + self[key] = val else: body.append(line) - pzl.body = ''.join(body) - return pzl + self.body = ''.join(body) + + def random_hash(self): + """Create a random hash from our number generator suitable for use as a filename.""" + return hashlib.sha1(str(self.rand.random()).encode('ascii')).digest() + + def _puzzle_file(self, path, name, visible=True): + """Make a puzzle file instance for the given file. + :param path: The path to the file + :param name: The name of the file. If set to None, the published file will have + a random hash as a name and have visible set to False. + :return: + """ + + # Make sure it actually exists. + if not os.path.exists(path): + raise ValueError("Included file {} does not exist.") + + file = open(path, 'rb') + + return PuzzleFile(path=path, handle=file, name=name, visible=visible) + + def make_file(self, name=None, mode='rw+b'): + """Get a file object for adding dynamically generated data to the puzzle. + :param name: The name of the file for links within the puzzle. If this is None, + the file will be hidden with a random hash as the name. + :return: A file object for writing + """ + + file = tempfile.TemporaryFile(mode=mode, delete=False) + + self._dict['files'].append(self._puzzle_file(file.name, name)) + + return file + + def __setitem__(self, key, value): + + if key in ('file', 'resource', 'hidden') and self._puzzle_dir is None: + raise KeyError("Cannot set a puzzle file for single file puzzles.") - def add(self, key, value): - super().add(key, value) if key == 'answer': - super().add(hash, djb2hash(value.encode('utf8'))) + # Handle adding answers to the puzzle + self._dict['hashes'].append(djb2hash(value.encode('utf8'))) + self._dict['answers'].append(value) + elif key == 'file': + # Handle adding files to the puzzle + path = os.path.join(self._puzzle_dir, 'files', value) + self._dict['files'][value] = self._puzzle_file(path, value) + elif key == 'resource': + # Handle adding category files to the puzzle + path = os.path.join(self._puzzle_dir, '../res', value) + self._dict['files'].append(self._puzzle_file(path, value)) + elif key == 'hidden': + # Handle adding secret, 'hidden' files to the puzzle. + path = os.path.join(self._puzzle_dir, 'files', value) + name = self.random_hash() + self._dict['files'].append(self._puzzle_file(path, name, visible=False)) + elif key in self.SINGULAR_KEYS: + # These keys can only have one value + self._dict[key] = value + elif key in self.KNOWN_KEYS: + self._dict[key].append(value) + else: + raise KeyError("Invalid Attribute: {}".format(key)) + + def __getitem__(self, item): + return self._dict[item] + + def make_answer(self, word_count, sep=b' '): + """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 bytes + """ + + if type(sep) == str: + sep = sep.encode('ascii') + + answer = sep.join(self.rand.sample(self.ANSWER_WORDS, word_count)) + self['answer'] = answer + + return answer + def htmlify(self): return mistune.markdown(self.body) - def publish(self): + def publish(self, dest): + """Deploy the puzzle to the given directory, and return the info needed for describing + the puzzle and accepting answers in MOTH.""" + + if not os.path.exists(dest): + raise ValueError("Puzzle destination directory does not exist.") + + # Delete the original directory + + # Save puzzle html file + + # Copy over all the files. + obj = { 'author': self['author'], - 'hashes': self.getall('hash'), + 'hashes': self['hashes'], 'body': self.htmlify(), } return obj def secrets(self): obj = { - 'answers': self.getall('answer'), + 'answers': self['answers'], 'summary': self['summary'], } return obj @@ -84,7 +240,7 @@ if __name__ == '__main__': filename = os.path.basename(puzzlePath) points, ext = os.path.splitext(filename) points = int(points) - puzzle = Puzzle.from_stream(open(puzzlePath)) + puzzle = Puzzle(puzzlePath, "test") puzzles[points] = puzzle for points in sorted(puzzles): From 0ac8d8e9ac893e2d9f1915d0d7df081f3d87fed7 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 18 Oct 2016 02:28:24 +0000 Subject: [PATCH 09/18] flake8 --- devel-server.py | 3 ++- mistune.py | 2 +- puzzles.py | 7 +++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/devel-server.py b/devel-server.py index 785bb3e..b1d0a3b 100755 --- a/devel-server.py +++ b/devel-server.py @@ -15,6 +15,7 @@ except ImportError: NOT_FOUND = 404 OK = 200 + def page(title, body): return """ @@ -120,7 +121,7 @@ you are a fool. self.serve_md() else: super().do_GET() - + def serve_md(self, text=None): fspathstr = self.translate_path(self.path) fspath = pathlib.Path(fspathstr) diff --git a/mistune.py b/mistune.py index c0f976d..a81c4c1 100644 --- a/mistune.py +++ b/mistune.py @@ -335,7 +335,7 @@ class BlockLexer(object): rest = len(item) if i != length - 1 and rest: - _next = item[rest-1] == '\n' + _next = item[rest - 1] == '\n' if not loose: loose = _next diff --git a/puzzles.py b/puzzles.py index 5b057d6..652b655 100644 --- a/puzzles.py +++ b/puzzles.py @@ -23,6 +23,7 @@ def djb2hash(buf): # to be done with Puzzle's random number generator, and it's cleaner to not pass that around. PuzzleFile = namedtuple('PuzzleFile', ['path', 'handle', 'name', 'visible']) + class Puzzle: KNOWN_KEYS = [ @@ -197,7 +198,6 @@ class Puzzle: return answer - def htmlify(self): return mistune.markdown(self.body) @@ -227,8 +227,8 @@ class Puzzle: 'summary': self['summary'], } return obj - -if __name__ == '__main__': + +if __name__ == '__main__': parser = argparse.ArgumentParser(description='Build a puzzle category') parser.add_argument('puzzledir', nargs='+', help='Directory of puzzle source') args = parser.parse_args() @@ -246,4 +246,3 @@ if __name__ == '__main__': for points in sorted(puzzles): puzzle = puzzles[points] print(puzzle.secrets()) - From 323f904e79dabe9b935be2d7487a330802980648 Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Mon, 17 Oct 2016 21:26:56 -0600 Subject: [PATCH 10/18] Added the ability to make a file just from a handle. Also, only accept category/points as puzzle paths. --- puzzles.py | 121 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 49 deletions(-) diff --git a/puzzles.py b/puzzles.py index 5b057d6..b31a8f7 100644 --- a/puzzles.py +++ b/puzzles.py @@ -12,7 +12,6 @@ import tempfile messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - def djb2hash(buf): h = 5381 for c in buf: @@ -23,15 +22,16 @@ def djb2hash(buf): # to be done with Puzzle's random number generator, and it's cleaner to not pass that around. PuzzleFile = namedtuple('PuzzleFile', ['path', 'handle', 'name', 'visible']) + class Puzzle: KNOWN_KEYS = [ - 'file', - 'resource', - 'temp_file', 'answer', - 'points', 'author', + 'file', + 'hidden', + 'name' + 'resource', 'summary' ] REQUIRED_KEYS = [ @@ -40,7 +40,7 @@ class Puzzle: 'points' ] SINGULAR_KEYS = [ - 'points' + 'name' ] # Get a big list of clean words for our answer file. @@ -48,6 +48,11 @@ class Puzzle: 'answer_words.txt'))] def __init__(self, path, category_seed): + """Puzzle objects need a path to a puzzle description ( + :param path: + :param category_seed: + """ + super().__init__() self._dict = defaultdict(lambda: []) @@ -60,32 +65,21 @@ class Puzzle: if not os.path.exists(path): raise ValueError("No puzzle at path: {]".format(path)) - elif os.path.isfile(path): - try: - # Expected format is path/.moth - self['points'] = int(os.path.split(path)[-1].split('.')[0]) - except (IndexError, ValueError): - raise ValueError("Invalid puzzle config. " - "Expected something like .moth") - - stream = open(path) - self._read_config(stream) elif os.path.isdir(path): + # Expected format is path/.moth + pathname = os.path.split(path)[-1] try: - # Expected format is path/.moth - self['points'] = int(os.path.split(path)[-1]) - except (IndexError, ValueError): - raise ValueError("Invalid puzzle config. Expected an integer point value for a " - "directory name.") - + self.points = int(pathname) + except ValueError: + pass files = os.listdir(path) - if 'config.moth' in files: - self._read_config(open(os.path.join(path, 'config.moth'))) + if 'puzzle.moth' in files: + self._read_config(open(os.path.join(path, 'puzzle.moth'))) - if 'make.py' in files: + if 'puzzle.py' in files: # Good Lord this is dangerous as fuck. - loader = SourceFileLoader('puzzle_mod', os.path.join(path, 'make.py')) + loader = SourceFileLoader('puzzle_mod', os.path.join(path, 'puzzle.py')) puzzle_mod = loader.load_module() if hasattr(puzzle_mod, 'make'): puzzle_mod.make(self) @@ -98,6 +92,18 @@ class Puzzle: # Set our 'files' as a dict, since we want register them uniquely by name. self['files'] = dict() + # A list of temporary files we've created that will need to be deleted. + self._temp_files = [] + + def cleanup(self): + """Cleanup any outstanding temporary files.""" + for path in self._temp_files: + if os.path.exists(path): + try: + os.unlink(path) + except OSError: + pass + def _read_config(self, stream): """Read a configuration file (ISO 2822)""" body = [] @@ -109,7 +115,6 @@ class Puzzle: header = False continue key, val = line.split(':', 1) - key = key.lower() val = val.strip() self[key] = val else: @@ -121,7 +126,11 @@ class Puzzle: return hashlib.sha1(str(self.rand.random()).encode('ascii')).digest() def _puzzle_file(self, path, name, visible=True): - """Make a puzzle file instance for the given file. + """Make a puzzle file instance for the given file. To add files as you would in the config + file (to 'file', 'hidden', or 'resource', simply assign to that keyword in the object: + puzzle['file'] = 'some_file.txt' + puzzle['hidden'] = 'some_hidden_file.txt' + puzzle['resource'] = 'some_file_in_the_category_resource_directory_omg_long_name.txt' :param path: The path to the file :param name: The name of the file. If set to None, the published file will have a random hash as a name and have visible set to False. @@ -136,20 +145,46 @@ class Puzzle: return PuzzleFile(path=path, handle=file, name=name, visible=visible) - def make_file(self, name=None, mode='rw+b'): - """Get a file object for adding dynamically generated data to the puzzle. - :param name: The name of the file for links within the puzzle. If this is None, - the file will be hidden with a random hash as the name. + def make_temp_file(self, name=None, mode='rw+b', 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 mode: The mode under which + :param visible: Whether or not the file will be visible to the user. :return: A file object for writing """ - file = tempfile.TemporaryFile(mode=mode, delete=False) + if name is None: + name = self.random_hash() - self._dict['files'].append(self._puzzle_file(file.name, name)) + file = tempfile.NamedTemporaryFile(mode=mode, delete=False) + file_read = open(file.name, 'rb') + + self._dict['files'][name] = PuzzleFile(path=file.name, handle=file_read, + name=name, visible=visible) return file + def make_handle_file(self, handle, name, visible=True): + """Add a file to the puzzle from a file handle. + :param handle: A file object or equivalent. + :param name: The name of the file in the final puzzle. + :param visible: Whether or not it's visible. + :return: None + """ + def __setitem__(self, key, value): + """Set a value for this puzzle, as if it were set in the config file. Most values default + being added to a list. Files (regardless of type) go in a dict under ['files']. Keys + in Puzzle.SINGULAR_KEYS are single values that get overwritten with subsequent assignments. + Only keys in Puzzle.KNOWN_KEYS are accepted. + :param key: + :param value: + :return: + """ + + key = key.lower() if key in ('file', 'resource', 'hidden') and self._puzzle_dir is None: raise KeyError("Cannot set a puzzle file for single file puzzles.") @@ -180,7 +215,7 @@ class Puzzle: raise KeyError("Invalid Attribute: {}".format(key)) def __getitem__(self, item): - return self._dict[item] + return self._dict[item.lower()] def make_answer(self, word_count, sep=b' '): """Generate and return a new answer. It's automatically added to the puzzle answer list. @@ -197,23 +232,11 @@ class Puzzle: return answer - def htmlify(self): + """Format and return the markdown for the puzzle body.""" return mistune.markdown(self.body) - def publish(self, dest): - """Deploy the puzzle to the given directory, and return the info needed for describing - the puzzle and accepting answers in MOTH.""" - - if not os.path.exists(dest): - raise ValueError("Puzzle destination directory does not exist.") - - # Delete the original directory - - # Save puzzle html file - - # Copy over all the files. - + def publish(self): obj = { 'author': self['author'], 'hashes': self['hashes'], From c10ad383ad9bfe9dc8daa2e89598521c581ca2fc Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 18 Oct 2016 05:02:05 +0000 Subject: [PATCH 11/18] devel-server use new Puzzles obj. Needs cleanup. --- devel-server.py | 30 +++++++++++++++--------------- puzzles.py | 30 +++++++++++++++++++++++++----- setup.cfg | 8 ++++++++ 3 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 setup.cfg diff --git a/devel-server.py b/devel-server.py index b1d0a3b..9ae36ff 100755 --- a/devel-server.py +++ b/devel-server.py @@ -1,4 +1,4 @@ -#!/usr/local/bin/python3 +#!/usr/bin/env python3 import glob import http.server @@ -15,6 +15,9 @@ except ImportError: NOT_FOUND = 404 OK = 200 +# XXX: This will eventually cause a problem. Do something more clever here. +seed = 1 + def page(title, body): return """ @@ -91,25 +94,22 @@ you are a fool. elif len(parts) == 3: # List all point values in a category body.append("# Puzzles in category `{}`".format(parts[2])) - puzz = [] - for i in glob.glob(os.path.join("puzzles", parts[2], "*.moth")): - base = os.path.basename(i) - root, _ = os.path.splitext(base) - points = int(root) - puzz.append(points) - for puzzle in sorted(puzz): - body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=puzzle)) + fpath = os.path.join("puzzles", parts[2]) + cat = puzzles.Category(fpath, seed) + for points in cat.pointvals: + body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=points)) elif len(parts) == 4: body.append("# {} puzzle {}".format(parts[2], parts[3])) - with open("puzzles/{}/{}.moth".format(parts[2], parts[3]), encoding="utf-8") as f: - p = puzzles.Puzzle(f) - body.append("* Author: `{}`".format(p.fields.get("author"))) - body.append("* Summary: `{}`".format(p.fields.get("summary"))) + fpath = os.path.join("puzzles", parts[2]) + cat = puzzles.Category(fpath, seed) + p = cat.puzzle(int(parts[3])) + body.append("* Author: `{}`".format(p['author'])) + body.append("* Summary: `{}`".format(p['summary'])) body.append('') body.append("## Body") body.append(p.body) - body.append("## Answers:") - for a in p.answers: + body.append("## Answers") + for a in p['answers']: body.append("* `{}`".format(a)) body.append("") else: diff --git a/puzzles.py b/puzzles.py index 652b655..57b2ddc 100644 --- a/puzzles.py +++ b/puzzles.py @@ -27,7 +27,7 @@ PuzzleFile = namedtuple('PuzzleFile', ['path', 'handle', 'name', 'visible']) class Puzzle: KNOWN_KEYS = [ - 'file', + 'files', 'resource', 'temp_file', 'answer', @@ -60,7 +60,7 @@ class Puzzle: self.body = '' if not os.path.exists(path): - raise ValueError("No puzzle at path: {]".format(path)) + raise ValueError("No puzzle at path: {}".format(path)) elif os.path.isfile(path): try: # Expected format is path/.moth @@ -81,8 +81,8 @@ class Puzzle: files = os.listdir(path) - if 'config.moth' in files: - self._read_config(open(os.path.join(path, 'config.moth'))) + if 'puzzle.moth' in files: + self._read_config(open(os.path.join(path, 'puzzle.moth'))) if 'make.py' in files: # Good Lord this is dangerous as fuck. @@ -93,7 +93,7 @@ class Puzzle: else: raise ValueError("Unacceptable file type for puzzle at {}".format(path)) - self._seed = hashlib.sha1(category_seed + bytes(self['points'])).digest() + self._seed = category_seed * self['points'] self.rand = random.Random(self._seed) # Set our 'files' as a dict, since we want register them uniquely by name. @@ -246,3 +246,23 @@ if __name__ == '__main__': for points in sorted(puzzles): puzzle = puzzles[points] print(puzzle.secrets()) + + +class Category: + def __init__(self, path, seed): + self.path = path + self.seed = seed + self.pointvals = [] + for fpath in glob.glob(os.path.join(path, "[0-9]*")): + pn = os.path.basename(fpath) + points = int(pn) + self.pointvals.append(points) + self.pointvals.sort() + + def puzzle(self, points): + path = os.path.join(self.path, str(points)) + return Puzzle(path, self.seed) + + def puzzles(self): + for points in self.pointvals: + yield self.puzzle(points) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3bf77b8 --- /dev/null +++ b/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 From 80b80e895c574dcabdfa415c31dac80e6356754d Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 18 Oct 2016 05:20:48 +0000 Subject: [PATCH 12/18] Make it work again --- devel-server.py | 4 ++-- puzzles.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/devel-server.py b/devel-server.py index 9ae36ff..9330761 100755 --- a/devel-server.py +++ b/devel-server.py @@ -146,9 +146,9 @@ you are a fool. self.wfile.write(content.encode('utf-8')) -def run(address=('', 8080)): +def run(address=('localhost', 8080)): httpd = ThreadingServer(address, MothHandler) - print("=== Listening on http://{}:{}/".format(address[0] or "localhost", address[1])) + print("=== Listening on http://{}:{}/".format(address[0], address[1])) httpd.serve_forever() if __name__ == '__main__': diff --git a/puzzles.py b/puzzles.py index 634049e..b481592 100644 --- a/puzzles.py +++ b/puzzles.py @@ -86,12 +86,9 @@ class Puzzle: else: raise ValueError("Unacceptable file type for puzzle at {}".format(path)) - self._seed = category_seed * self['points'] + self._seed = category_seed * self.points self.rand = random.Random(self._seed) - # Set our 'files' as a dict, since we want register them uniquely by name. - self['files'] = dict() - # A list of temporary files we've created that will need to be deleted. self._temp_files = [] From 0cd6dc97c5828f1113bc81ef468a282635675579 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 18 Oct 2016 09:34:06 -0600 Subject: [PATCH 13/18] Send tracebacks to browser --- devel-server.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/devel-server.py b/devel-server.py index 9330761..5543f83 100755 --- a/devel-server.py +++ b/devel-server.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import cgi import glob import http.server import mistune @@ -7,6 +8,8 @@ import os import pathlib import puzzles import socketserver +import sys +import traceback try: from http.server import HTTPStatus @@ -48,7 +51,20 @@ class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): pass -class MothHandler(http.server.CGIHTTPRequestHandler): +class MothHandler(http.server.SimpleHTTPRequestHandler): + def handle_one_request(self): + try: + super().handle_one_request() + except: + tbtype, value, tb = sys.exc_info() + tblist = traceback.format_tb(tb, None) + traceback.format_exception_only(tbtype, value) + page = ("# Traceback (most recent call last)\n" + + " " + + " ".join(tblist[:-1]) + + tblist[-1]) + self.serve_md(page) + + def do_GET(self): if self.path == "/": self.serve_front() From 881e5a46e1053f815812a3eee2ba0cc263e25a3e Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 18 Oct 2016 09:51:33 -0600 Subject: [PATCH 14/18] Get generated puzzles working --- puzzles.py | 54 ++++++++++++++++++++++++++---------------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/puzzles.py b/puzzles.py index b481592..f9b78cd 100644 --- a/puzzles.py +++ b/puzzles.py @@ -55,6 +55,9 @@ class Puzzle: super().__init__() + if not os.path.isdir(path): + raise ValueError("No such directory: {}".format(path)) + self._dict = defaultdict(lambda: []) if os.path.isdir(path): self._puzzle_dir = path @@ -63,34 +66,32 @@ class Puzzle: self.message = bytes(random.choice(messageChars) for i in range(20)) self.body = '' - if not os.path.exists(path): - raise ValueError("No puzzle at path: {}".format(path)) - elif os.path.isdir(path): - # Expected format is path/.moth - pathname = os.path.split(path)[-1] - try: - self.points = int(pathname) - except ValueError: - pass - files = os.listdir(path) + # A list of temporary files we've created that will need to be deleted. + self._temp_files = [] - if 'puzzle.moth' in files: - self._read_config(open(os.path.join(path, 'puzzle.moth'))) - - if 'puzzle.py' in files: - # Good Lord this is dangerous as fuck. - loader = SourceFileLoader('puzzle_mod', os.path.join(path, 'puzzle.py')) - puzzle_mod = loader.load_module() - if hasattr(puzzle_mod, 'make'): - puzzle_mod.make(self) - else: - raise ValueError("Unacceptable file type for puzzle at {}".format(path)) + # Expected format is path/.moth + pathname = os.path.split(path)[-1] + try: + self.points = int(pathname) + except ValueError: + raise ValueError("Directory name must be a point value: {}".format(path)) + files = os.listdir(path) self._seed = category_seed * self.points self.rand = random.Random(self._seed) - # A list of temporary files we've created that will need to be deleted. - self._temp_files = [] + if 'puzzle.moth' in files: + self._read_config(open(os.path.join(path, 'puzzle.moth'))) + + if 'puzzle.py' in files: + # Good Lord this is dangerous as fuck. + loader = SourceFileLoader('puzzle_mod', os.path.join(path, 'puzzle.py')) + puzzle_mod = loader.load_module() + if hasattr(puzzle_mod, 'make'): + self.body = '# `puzzle.body` was not set by the `make` function' + puzzle_mod.make(self) + else: + self.body = '# `puzzle.py` does not define a `make` function' def cleanup(self): """Cleanup any outstanding temporary files.""" @@ -214,16 +215,13 @@ class Puzzle: def __getitem__(self, item): return self._dict[item.lower()] - def make_answer(self, word_count, sep=b' '): + def make_answer(self, word_count, 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 bytes + :returns: The answer string """ - if type(sep) == str: - sep = sep.encode('ascii') - answer = sep.join(self.rand.sample(self.ANSWER_WORDS, word_count)) self['answer'] = answer From 21306027b766f6463d0387e7fdfe57bcecd24329 Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Tue, 18 Oct 2016 11:24:46 -0600 Subject: [PATCH 15/18] Minor bug fix, documentation added. --- puzzles.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/puzzles.py b/puzzles.py index b31a8f7..c9ba825 100644 --- a/puzzles.py +++ b/puzzles.py @@ -47,10 +47,21 @@ class Puzzle: ANSWER_WORDS = [w.strip() for w in open(os.path.join(os.path.dirname(__file__), 'answer_words.txt'))] - def __init__(self, path, category_seed): - """Puzzle objects need a path to a puzzle description ( - :param path: - :param category_seed: + def __init__(self, category_seed, path=None): + """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 path: An optional path to a puzzle directory. The point value for the puzzle is taken + from the puzzle directories name (it must be an integer greater than zero). + Within this directory, we expect: + (optional) A puzzle.moth file in RFC2822 format. The puzzle will get its attributes + from the headers, and the body will be the puzzle description in + Markdown format. + (optional) A puzzle.py file. This is expected to have a callable called make + that takes a single positional argument (this puzzle object). + This callable can then do whatever it needs to with this object. + If neither of the above are given, the point value for the puzzle will have to + be set manually. """ super().__init__() @@ -63,6 +74,16 @@ class Puzzle: self.message = bytes(random.choice(messageChars) for i in range(20)) self.body = '' + self._seed = hashlib.sha1(category_seed + bytes(self['points'])).digest() + self.rand = random.Random(self._seed) + + # Set our 'files' as a dict, since we want register them uniquely by name. + self['files'] = dict() + + # A list of temporary files we've created that will need to be deleted. + self._temp_files = [] + + # All internal variables must be initialized before the following runs if not os.path.exists(path): raise ValueError("No puzzle at path: {]".format(path)) elif os.path.isdir(path): @@ -86,15 +107,6 @@ class Puzzle: else: raise ValueError("Unacceptable file type for puzzle at {}".format(path)) - self._seed = hashlib.sha1(category_seed + bytes(self['points'])).digest() - self.rand = random.Random(self._seed) - - # Set our 'files' as a dict, since we want register them uniquely by name. - self['files'] = dict() - - # A list of temporary files we've created that will need to be deleted. - self._temp_files = [] - def cleanup(self): """Cleanup any outstanding temporary files.""" for path in self._temp_files: From 1f9e63cfa6aa2fed38d0db6ae7be5fdc47cca1bc Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Tue, 18 Oct 2016 13:11:53 -0600 Subject: [PATCH 16/18] Minor bug fix, documentation added. --- puzzles.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/puzzles.py b/puzzles.py index c9ba825..5b3bd98 100644 --- a/puzzles.py +++ b/puzzles.py @@ -21,6 +21,15 @@ def djb2hash(buf): # We use a named tuple rather than a full class, because any random name generation has # to be done with Puzzle's random number generator, and it's cleaner to not pass that around. PuzzleFile = namedtuple('PuzzleFile', ['path', 'handle', 'name', 'visible']) +PuzzleFile.__doc__ = """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.""" class Puzzle: From db78f43324e3851c7f82bcc75f2ad911e6673088 Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Tue, 18 Oct 2016 13:29:41 -0600 Subject: [PATCH 17/18] Added a bit more documentation. Changed 'answers' to 'answer', 'hashes' to 'hash' for consistency. --- puzzles.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/puzzles.py b/puzzles.py index 94741ce..3923f89 100644 --- a/puzzles.py +++ b/puzzles.py @@ -46,7 +46,6 @@ class Puzzle: REQUIRED_KEYS = [ 'author', 'answer', - 'points' ] SINGULAR_KEYS = [ 'name' @@ -57,7 +56,7 @@ class Puzzle: 'answer_words.txt'))] def __init__(self, category_seed, path=None, points=None): - """A MOTH Puzzle + """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 path: An optional path to a puzzle directory. The point value for the puzzle is taken @@ -72,6 +71,14 @@ class Puzzle: :param points: The point value of the puzzle. Mutually exclusive with path. If neither of the above are given, the point value for the puzzle will have to be set at instantiation. + + For puzzle attributes, this class acts like a dictionary that in most cases assigns + always returns a list. Certain keys, however behave differently: + - Keys in Puzzle.SINGULAR_KEYS can only have one value, and writing to these overwrites + that value. + - The keys 'hidden', 'file', and 'resource' all create a new PuzzleFile object that + gets added under the 'files' key. + - The 'answer' also adds a new hash under the the 'hash' key. """ super().__init__() @@ -216,8 +223,8 @@ class Puzzle: if key == 'answer': # Handle adding answers to the puzzle - self._dict['hashes'].append(djb2hash(value.encode('utf8'))) - self._dict['answers'].append(value) + self._dict['hash'].append(djb2hash(value.encode('utf8'))) + self._dict['answer'].append(value) elif key == 'file': # Handle adding files to the puzzle path = os.path.join(self._puzzle_dir, 'files', value) From 10eaf13a4eb8c07369edcb8633aca73a32db1201 Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Tue, 18 Oct 2016 20:11:20 -0600 Subject: [PATCH 18/18] The devel server now serves puzzle files. --- devel-server.py | 58 +++++++++++++++++++++++++++++++++++++++---------- puzzles.py | 12 +++++----- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/devel-server.py b/devel-server.py index 5543f83..0f15dfd 100755 --- a/devel-server.py +++ b/devel-server.py @@ -102,35 +102,69 @@ you are a fool. body = [] path = self.path.rstrip('/') parts = path.split("/") + #raise ValueError(parts) if len(parts) < 3: # List all categories body.append("# Puzzle Categories") for i in glob.glob(os.path.join("puzzles", "*", "")): body.append("* [{}](/{})".format(i, i)) - elif len(parts) == 3: + self.serve_md('\n'.join(body)) + return + + fpath = os.path.join("puzzles", parts[2]) + cat = puzzles.Category(fpath, seed) + if len(parts) == 3: # List all point values in a category body.append("# Puzzles in category `{}`".format(parts[2])) - fpath = os.path.join("puzzles", parts[2]) - cat = puzzles.Category(fpath, seed) for points in cat.pointvals: body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=points)) - elif len(parts) == 4: + self.serve_md('\n'.join(body)) + return + + pzl = cat.puzzle(int(parts[3])) + if len(parts) == 4: body.append("# {} puzzle {}".format(parts[2], parts[3])) - fpath = os.path.join("puzzles", parts[2]) - cat = puzzles.Category(fpath, seed) - p = cat.puzzle(int(parts[3])) - body.append("* Author: `{}`".format(p['author'])) - body.append("* Summary: `{}`".format(p['summary'])) + body.append("* Author: `{}`".format(pzl['author'])) + body.append("* Summary: `{}`".format(pzl['summary'])) body.append('') body.append("## Body") - body.append(p.body) + body.append(pzl.body) body.append("## Answers") - for a in p['answers']: + for a in pzl['answer']: body.append("* `{}`".format(a)) body.append("") + body.append("## Files") + for pzl_file in pzl['files']: + body.append("* [puzzles/{cat}/{points}/{filename}]({filename})" + .format(cat=parts[2], points=pzl.points, filename=pzl_file)) + self.serve_md('\n'.join(body)) + return + elif len(parts) == 5: + try: + self.serve_puzzle_file(pzl['files'][parts[4]]) + except KeyError: + self.send_error(HTTPStatus.NOT_FOUND, "File not found") + return else: body.append("# Not Implemented Yet") - self.serve_md('\n'.join(body)) + self.serve_md('\n'.join(body)) + + CHUNK_SIZE = 4096 + def serve_puzzle_file(self, file): + """Serve a PuzzleFile object.""" + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", "application/octet-stream") + self.send_header('Content-Disposition', 'attachment; filename="{}"'.format(file.name)) + if file.path is not None: + fs = os.stat(file.path) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + + # We're using application/octet stream, so we can send the raw bytes. + self.end_headers() + chunk = file.handle.read(self.CHUNK_SIZE) + while chunk: + self.wfile.write(chunk) + chunk = file.handle.read(self.CHUNK_SIZE) def serve_file(self): if self.path.endswith(".md"): diff --git a/puzzles.py b/puzzles.py index 3923f89..df18c0c 100644 --- a/puzzles.py +++ b/puzzles.py @@ -94,6 +94,9 @@ class Puzzle: self.message = bytes(random.choice(messageChars) for i in range(20)) self.body = '' + # This defaults to a dict, not a list like most things + self._dict['files'] = {} + # A list of temporary files we've created that will need to be deleted. self._temp_files = [] if path is not None: @@ -171,18 +174,17 @@ class Puzzle: # Make sure it actually exists. if not os.path.exists(path): - raise ValueError("Included file {} does not exist.") + raise ValueError("Included file {} does not exist.".format(path)) file = open(path, 'rb') return PuzzleFile(path=path, handle=file, name=name, visible=visible) - def make_temp_file(self, name=None, mode='rw+b', visible=True): + 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 mode: The mode under which :param visible: Whether or not the file will be visible to the user. :return: A file object for writing """ @@ -190,7 +192,7 @@ class Puzzle: if name is None: name = self.random_hash() - file = tempfile.NamedTemporaryFile(mode=mode, delete=False) + file = tempfile.NamedTemporaryFile(mode='w+b', delete=False) file_read = open(file.name, 'rb') self._dict['files'][name] = PuzzleFile(path=file.name, handle=file_read, @@ -313,7 +315,7 @@ class Category: def puzzle(self, points): path = os.path.join(self.path, str(points)) - return Puzzle(path, self.seed) + return Puzzle(self.seed, path=path) def puzzles(self): for points in self.pointvals: