Browser and install GUI for cookiecutter templates

βŒˆβŒ‹ βŽ‡ branch:  cookiedough


Check-in [01853f4400]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Enable replay option, add verbose setting, use full pluginconf config[] list structure now (select: and value: just the default), keep all _control and __private vars around; but omit them from default context for invocation. Add startup progressbar.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 01853f4400d947354d6b9c80099ca25e6744d42f37d9f12baac635844ce6730f
User & Date: mario 2021-03-24 09:08:27
Context
2021-03-24
09:13
Minor updates to help/ check-in: 41f2f2556e user: mario tags: trunk
09:08
Enable replay option, add verbose setting, use full pluginconf config[] list structure now (select: and value: just the default), keep all _control and __private vars around; but omit them from default context for invocation. Add startup progressbar. check-in: 01853f4400 user: mario tags: trunk
2021-03-23
15:26
serialize JSON without Unicode escapes, release as 0.1.0 check-in: 8a032a1e3c user: mario tags: trunk, 0.1.0
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to cookiedough/__init__.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3
# encoding: utf-8
# fmt: off
# api: python
# type: gui
# title: cookiedough
# description: browser and install GUI for cookiecutter templates
# category: viewer
# version: 0.1.0
# state:   alpha
# license: proprietary
# config:
#     { name: colorize, type: bool, value: 1, description: Colorize the README preview, help: Basically just highlighting of headlines and code blocks. Display is marginally faster if disabled. }
#     { name: sort, type: select, value: all, select: all|size|stars|forks|name|short|vars|files|updated_at, description: Primary sorting property, help: Uses internal scores, or properties like the Β»shortΒ« name. }
#     { name: show_counts, type: bool, value: 1, description: Show number of entries per language/api category, help: Else just the names. }
#     { name: search_keypress, type: bool, value: 0, description: Search on any keypress - instead of just Enter., help: Requires restarting cookiedough. }








|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3
# encoding: utf-8
# fmt: off
# api: python
# type: gui
# title: cookiedough
# description: browser and install GUI for cookiecutter templates
# category: viewer
# version: 0.1.5
# state:   alpha
# license: proprietary
# config:
#     { name: colorize, type: bool, value: 1, description: Colorize the README preview, help: Basically just highlighting of headlines and code blocks. Display is marginally faster if disabled. }
#     { name: sort, type: select, value: all, select: all|size|stars|forks|name|short|vars|files|updated_at, description: Primary sorting property, help: Uses internal scores, or properties like the Β»shortΒ« name. }
#     { name: show_counts, type: bool, value: 1, description: Show number of entries per language/api category, help: Else just the names. }
#     { name: search_keypress, type: bool, value: 0, description: Search on any keypress - instead of just Enter., help: Requires restarting cookiedough. }
62
63
64
65
66
67
68
69

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

85
86
87

88
89

90
91
92
93
94
95
96
    "sort": "all",
    "show_counts": True,
    "search_keypress": False,
    "no_params": False,
    "update_ccjson": True,
    "hook_prompt": True,
    "editor": "mousepad",
#    "replay": 1,

    "theme": "DarkBlue2",
    "conf_file": appdirs.user_config_dir("cookiedough", "io") + "/settings.json",
    "plugins": { "__init__": 1, "rollout": 1, "icons": 1 }
}
try:
    conf.update(json.load(open(conf['conf_file'], "r", encoding="utf-8")))
except:
    pass
conf["debug"] = "--debug" in sys.argv
rollout.conf = conf


#-- JSON blob of templates
class repos():
    def __init__(self):

        # load data file
        fn = re.sub("[\w.]+$", "uidata.json", __file__)
        self.ls = json.load(open(fn, "r", encoding="utf-8"))

        # prepare sorting
        [update.score(d) for d in self.ls.values()]

        
    def tree(self, ls=None):
        """ Convert to tree list """
        if not ls:
            ls = self.ls
        if isinstance(ls, dict):
            ls = ls.values()







|
>















>



>


>







62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
    "sort": "all",
    "show_counts": True,
    "search_keypress": False,
    "no_params": False,
    "update_ccjson": True,
    "hook_prompt": True,
    "editor": "mousepad",
    "replay": True,
    "verbose": False,
    "theme": "DarkBlue2",
    "conf_file": appdirs.user_config_dir("cookiedough", "io") + "/settings.json",
    "plugins": { "__init__": 1, "rollout": 1, "icons": 1 }
}
try:
    conf.update(json.load(open(conf['conf_file'], "r", encoding="utf-8")))
except:
    pass
conf["debug"] = "--debug" in sys.argv
rollout.conf = conf


#-- JSON blob of templates
class repos():
    def __init__(self):
        update.progress(5)
        # load data file
        fn = re.sub("[\w.]+$", "uidata.json", __file__)
        self.ls = json.load(open(fn, "r", encoding="utf-8"))
        update.progress(20)
        # prepare sorting
        [update.score(d) for d in self.ls.values()]
        update.progress(50)
        
    def tree(self, ls=None):
        """ Convert to tree list """
        if not ls:
            ls = self.ls
        if isinstance(ls, dict):
            ls = ls.values()
216
217
218
219
220
221
222

223
224
225
226
227

228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245

246
247
248
249
250
251
252
        #log.init.info("build window")
        sg.theme(conf["theme"])
        self.w = sg.Window(
            title=f"cookiedough", layout=layout, font="Sans 12",
            size=(1080,725), margins=(0,0), resizable=False, use_custom_titlebar=False,
            background_color="#fafafa", icon=icons.icon#, ttk_theme="yaru"
        )

        self.win_map = {}
        # alias functions
        self.status = self.w["status"].update
        # widget patching per tk
        self.w.read(timeout=1)

        self.w["menu"].Widget.configure(borderwidth=0, type="menubar")
        self.w["template"].Widget.configure(show="tree") # borderwidth=0
        self.w["search"].Widget.bind("<Return>", self.search_enter)
        self.w["bb1"].Widget.configure(borderwidth=0)
        self.w["bb2"].Widget.configure(borderwidth=0)
        self.w["template"].set_focus()

    
   # add to *win_map{} event loop
    def win_register(self, win, cb=None):
        if not cb:
            def cb(event, data):
                win.close()
        self.win_map[win] = cb
        win.read(timeout=1)

    # demultiplex PySimpleGUI events across multiple windows
    def main(self):

        self.win_register(self.w, self.event)
        while True:
            win_ls = [win for win in self.win_map.keys()]
            #log.event_loop.win_ls_length.debug(len(win_ls))
            # unlink closed windows
            for win in win_ls:
                if win.TKrootDestroyed:







>





>


















>







220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
        #log.init.info("build window")
        sg.theme(conf["theme"])
        self.w = sg.Window(
            title=f"cookiedough", layout=layout, font="Sans 12",
            size=(1080,725), margins=(0,0), resizable=False, use_custom_titlebar=False,
            background_color="#fafafa", icon=icons.icon#, ttk_theme="yaru"
        )
        update.progress(65)
        self.win_map = {}
        # alias functions
        self.status = self.w["status"].update
        # widget patching per tk
        self.w.read(timeout=1)
        update.progress(90)
        self.w["menu"].Widget.configure(borderwidth=0, type="menubar")
        self.w["template"].Widget.configure(show="tree") # borderwidth=0
        self.w["search"].Widget.bind("<Return>", self.search_enter)
        self.w["bb1"].Widget.configure(borderwidth=0)
        self.w["bb2"].Widget.configure(borderwidth=0)
        self.w["template"].set_focus()

    
   # add to *win_map{} event loop
    def win_register(self, win, cb=None):
        if not cb:
            def cb(event, data):
                win.close()
        self.win_map[win] = cb
        win.read(timeout=1)

    # demultiplex PySimpleGUI events across multiple windows
    def main(self):
        update.progress(100)
        self.win_register(self.w, self.event)
        while True:
            win_ls = [win for win in self.win_map.keys()]
            #log.event_loop.win_ls_length.debug(len(win_ls))
            # unlink closed windows
            for win in win_ls:
                if win.TKrootDestroyed:

Changes to cookiedough/rollout.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19






20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35


36
37
38
39
40
41
42

43
44
45
46
47
48
49
50
51
52




53
54
55
56
57
58
59
60
61
# encoding: utf-8
# api: cookiedough
# type: function
# title: roll out
# description: deploy slected cookiecutter template
# category: action
# version: 0.4
# config:
#     { name: no_params, type: bool, value: 0, description: Don't prompt for template vars. Use terminal prompts instead., help: You mgiht as well use cookiecutter directly then. }
#     { name: update_ccjson, type: bool, value: 1, description: Update parameters from cookiecutter.json files, help: avoids extra prompts if template infos outdated }
#     { name: hook_prompt, type: bool, value: 1, description: Display any additional prompts as GUI inputs, help: hook prompt.* functions }
# disabled-config:
#     { name: replay, type: bool, value: 1, description: Use replay/ variables, help: Overrides default values with previous inputs for the same tempalte. }
# priority: core
# depends: python:cookiecutter
#
# Implements the parameter input window before incoking cookiecutter(1)
# for extracting a template. Also hooks cookiecutter.prompt.* functions,
# to avoid terminal prompts.






#


import sys, os, re, json
import PySimpleGUI as sg
import requests, appdirs
from cookiedough import icons
from traceback import format_exc
from textwrap import dedent


# joined to main.conf
conf = {
    "no_params": False,
    "update_ccjson": True,
    "hook_prompt": True,


}


# finally, this is where cookiecutter gets invoked,
# params={} from the input window
# doc: https://cookiecutter.readthedocs.io/en/1.7.2/advanced/calling_from_python.html
def cutting(repo_url, params):

    # params
    ccc = CookieCutterConfig()
    if m := re.match("^(.+)\?(?:d|dir|directory)=(.+)$", repo_url):
        repo_url, directory = m.groups() # from http://repo.git/?dir=template2/
    else:
        directory = None
    if params:
        no_input = True
    else:
        no_input = False # from conf[no_params], set by task.__init__




    # run
    import cookiecutter.main
    dir = cookiecutter.main.cookiecutter(
        template=repo_url,
        #checkout=None,
        no_input=no_input,
        extra_context=params,
        #replay=None,
        #overwrite_if_exists=False,




|

|

|


|
|



|

|
>
>
>
>
>
>
















>
>







>










>
>
>
>

<







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
# encoding: utf-8
# api: cookiedough
# type: function
# title: roll out
# description: deploy selected cookiecutter template
# category: action
# version: 0.5
# config:
#     { name: replay, type: bool, value: 1, description: Use replay/ variables as defaults, help: Overrides input variables with previous inputs for the same template. }
#     { name: update_ccjson, type: bool, value: 1, description: Update parameters from cookiecutter.json files, help: avoids extra prompts if template infos outdated }
#     { name: hook_prompt, type: bool, value: 1, description: Display any additional prompts as GUI inputs, help: hook prompt.* functions }
#     { name: no_params, type: bool, value: 0, description: Don't prompt for template vars. Use terminal prompts instead., help: You might as well use cookiecutter directly then. }
#     { name: verbose, type: bool, value: 0, description: cookiecutter --verbose for more details on extraction, help: Will print any output to console }
# priority: core
# depends: python:cookiecutter
#
# Implements the parameter input window before invoking cookiecutter(1)
# for extracting a template. Also hooks cookiecutter.prompt.* functions,
# to avoid any extra terminal prompts.
#
# Additionally updates config[] dict from remote cookiecutter.json defaults
# and previous ~/.config/cookiecutter/replay/*.json input.
#
# Also contains the patch logic to modify cookiecutter to use ~/.config
# per default.
#


import sys, os, re, json
import PySimpleGUI as sg
import requests, appdirs
from cookiedough import icons
from traceback import format_exc
from textwrap import dedent


# joined to main.conf
conf = {
    "no_params": False,
    "update_ccjson": True,
    "hook_prompt": True,
    "replay": True,
    "verbose": False,
}


# finally, this is where cookiecutter gets invoked,
# params={} from the input window
# doc: https://cookiecutter.readthedocs.io/en/1.7.2/advanced/calling_from_python.html
def cutting(repo_url, params):
    import cookiecutter.main
    # params
    ccc = CookieCutterConfig()
    if m := re.match("^(.+)\?(?:d|dir|directory)=(.+)$", repo_url):
        repo_url, directory = m.groups() # from http://repo.git/?dir=template2/
    else:
        directory = None
    if params:
        no_input = True
    else:
        no_input = False # from conf[no_params], set by task.__init__
    # inject verbose flag (alternatively: use cookiercutter.cli instead of .main)
    if conf.get("verbose"):
        import cookiecutter.log
        cookiecutter.log.configure_logger(stream_level='DEBUG', debug_file=None)
    # run

    dir = cookiecutter.main.cookiecutter(
        template=repo_url,
        #checkout=None,
        no_input=no_input,
        extra_context=params,
        #replay=None,
        #overwrite_if_exists=False,
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144

# scan local ~/.config/cookiecutter/replay/* dir, get override values from there
def update_replay(d):
    replay = {}
    try:
        m = re.search("(/[\w\-]+)(\.git|\.zip)?(\?.+$|$)", d["repo"])
        fn = CookieCutterConfig().replay + m[1] + ".json"
        print(fn)
        with open(fn, "r", encoding="utf-8") as f:
            replay = json.load(f)["cookiecutter"]
        # we could check if _template matches up with d[repo] at this point (likely couldn't account for ?directory=)
    except:
        return
    for e in d["config"]:
        if e["type"] not in ("str", "int", ):







<







142
143
144
145
146
147
148

149
150
151
152
153
154
155

# scan local ~/.config/cookiecutter/replay/* dir, get override values from there
def update_replay(d):
    replay = {}
    try:
        m = re.search("(/[\w\-]+)(\.git|\.zip)?(\?.+$|$)", d["repo"])
        fn = CookieCutterConfig().replay + m[1] + ".json"

        with open(fn, "r", encoding="utf-8") as f:
            replay = json.load(f)["cookiecutter"]
        # we could check if _template matches up with d[repo] at this point (likely couldn't account for ?directory=)
    except:
        return
    for e in d["config"]:
        if e["type"] not in ("str", "int", ):
182
183
184
185
186
187
188

189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214








215

216
217
218
219
220
221
222
223
224
225
    # convert config[] list to input widgets
    def fields(self, cfg):
        bg = { "background_color": "#2980b9" }
        pad = { "background_color": "#c6d7e3", "pad": ((20,2),(0)) }
        ls = []
        for e in cfg:
            ls.append(sg.Text(e["name"], font="Sans 12 bold", pad=((10,13),(10,2)), **bg))

            if e["type"] == "select":
                values = e.get("select") or e.get("value") or [""]
                ls.append(sg.Combo(values, default_value=values[0], key=e["name"], font="Roboto 12", text_color="black", **pad))
            elif e["type"] == "dict":
                # display only, not editable, and cookiecutter will source it from the cc.json
                ls.append(sg.Multiline(json.dumps(e["value"], indent=2), size=(50,3), disabled=True, text_color="black", **pad))
            else:
                ls.append(sg.Input(e["value"], key=e["name"], font="Roboto 11", text_color="black", **pad))
            if e.get("description"):
                ls.append(sg.Text(e["description"], font="Sans 9", text_color="#444", pad=(20,1), **bg))
        ls.append(sg.T("", **bg))
        return ls

    # window actions
    def event(self, event, data):
        if event == "#cancel":
            self.w.close()
        elif event == "#chdir":
            self.main.working_directory(...)
        elif event == "#bake":
            params = {k:v for k,v in data.items() if re.match("^\w+", k)}
            if params.get("_extensions"):
                for e in self.d["config"]:
                    if e["name"] == "_extensions":
                        params["_extensions"] = e["value"]
            self.w.close()








            dir = cutting(self.d["repo"], params)

            print("cookiecutting done.")
            self.open_target(dir)
            self.main.status(f"{dir} created")

    # open extracted dir
    def open_target(self, dir):
        if os.path.exists(dir):
            os.system("xdg-open %r &" % dir)









>


|




|












<
<
<
<
<

>
>
>
>
>
>
>
>
|
>

|
|







193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220





221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
    # convert config[] list to input widgets
    def fields(self, cfg):
        bg = { "background_color": "#2980b9" }
        pad = { "background_color": "#c6d7e3", "pad": ((20,2),(0)) }
        ls = []
        for e in cfg:
            ls.append(sg.Text(e["name"], font="Sans 12 bold", pad=((10,13),(10,2)), **bg))
            _disabled = e["class"] != "cookiecutter"
            if e["type"] == "select":
                values = e.get("select") or e.get("value") or [""]
                ls.append(sg.Combo(values, default_value=e["value"], key=e["name"], font="Roboto 12", text_color="black", disabled=_disabled, **pad))
            elif e["type"] == "dict":
                # display only, not editable, and cookiecutter will source it from the cc.json
                ls.append(sg.Multiline(json.dumps(e["value"], indent=2), size=(50,3), disabled=True, text_color="black", **pad))
            else:
                ls.append(sg.Input(e["value"], key=e["name"], font="Roboto 11", text_color="black", disabled=_disabled, **pad))
            if e.get("description"):
                ls.append(sg.Text(e["description"], font="Sans 9", text_color="#444", pad=(20,1), **bg))
        ls.append(sg.T("", **bg))
        return ls

    # window actions
    def event(self, event, data):
        if event == "#cancel":
            self.w.close()
        elif event == "#chdir":
            self.main.working_directory(...)
        elif event == "#bake":





            self.w.close()
            self.bake(data)

    # invoke cookiecutter from data{} widget values
    def bake(self, data):
        params = {
           # assemble all but _control and __private vars (cc will apply those itself)
           k:v  for  k,v  in data.items()  if  re.match("^[a-z]+", k, re.I)
        }
        dir = cutting(self.d["repo"], params)
        if not conf.get("verbose"):
            print("cookiecutting done.")
        self.open_target(dir)
        self.main.status(f"{dir} created")

    # open extracted dir
    def open_target(self, dir):
        if os.path.exists(dir):
            os.system("xdg-open %r &" % dir)


Changes to cookiedough/update.py.

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 
# This is where the dev/ scripts might end up, so that GH/BB/GL could be
# polled from the main window. For now it's just for the scoring algorithm.
# (Basically looks for averages, some benefits for documentation quality.)
#
# 

import re, time

# local settings (not joined with main)
conf = {
    "score.find": "Makefile | NEWS | CHANGES(?:\.md|\.rst)? | \.fpm(?:rc)?",
    "date": time.strftime("%Y-%m-%d"),
}








|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 
# This is where the dev/ scripts might end up, so that GH/BB/GL could be
# polled from the main window. For now it's just for the scoring algorithm.
# (Basically looks for averages, some benefits for documentation quality.)
#
# 

import re, time, sys

# local settings (not joined with main)
conf = {
    "score.find": "Makefile | NEWS | CHANGES(?:\.md|\.rst)? | \.fpm(?:rc)?",
    "date": time.strftime("%Y-%m-%d"),
}

124
125
126
127
128
129
130










    'luismartingil/cookiecutter-beamer', 'JonasGroeger/cookiecutter-mediawiki-extension', 'kkujawinski/cookiecutter-sublime-text-3-plugin', 'fhightower-templates/sublime-snippet-package-template',
    'mahmoudimus/cookiecutter-slim-berkshelf-vagrant', 'audreyr/cookiecutter-complexity', 'keimlink/cookiecutter-reveal.js', 'relekang/cookiecutter-tumblr-theme', 'Plippe/cookiecutter-scala',
    'jpzk/cookiecutter-scala-spark', 'joeyjoejoejr/cookiecutter-atari2600', 'jupyter-widgets/widget-cookiecutter', 'drivendata/cookiecutter-data-science', 'bdcaf/cookiecutter-r-data-analysis',
    'docker-science/cookiecutter-docker-science', 'mkrapp/cookiecutter-reproducible-science', 'jastark/cookiecutter-data-driven-journalism', 'painless-software/painless-continuous-delivery',
    'DualSpark/cookiecutter-tf-module', 'hkage/cookiecutter-tornado', 'Pawamoy/cookiecutter-awesome', 'sindresorhus/awesome', 'bdcaf/cookiecutter_dotfile', 'genzj/cookiecutter-raml']
    return name in ids


















>
>
>
>
>
>
>
>
>
>
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
    'luismartingil/cookiecutter-beamer', 'JonasGroeger/cookiecutter-mediawiki-extension', 'kkujawinski/cookiecutter-sublime-text-3-plugin', 'fhightower-templates/sublime-snippet-package-template',
    'mahmoudimus/cookiecutter-slim-berkshelf-vagrant', 'audreyr/cookiecutter-complexity', 'keimlink/cookiecutter-reveal.js', 'relekang/cookiecutter-tumblr-theme', 'Plippe/cookiecutter-scala',
    'jpzk/cookiecutter-scala-spark', 'joeyjoejoejr/cookiecutter-atari2600', 'jupyter-widgets/widget-cookiecutter', 'drivendata/cookiecutter-data-science', 'bdcaf/cookiecutter-r-data-analysis',
    'docker-science/cookiecutter-docker-science', 'mkrapp/cookiecutter-reproducible-science', 'jastark/cookiecutter-data-driven-journalism', 'painless-software/painless-continuous-delivery',
    'DualSpark/cookiecutter-tf-module', 'hkage/cookiecutter-tornado', 'Pawamoy/cookiecutter-awesome', 'sindresorhus/awesome', 'bdcaf/cookiecutter_dotfile', 'genzj/cookiecutter-raml']
    return name in ids


# progressbar from rich "β–°β–°β–°β–°β–°β–°β–°β–°β–±β–±β–±β–±β–±"
def progress(i=1, n=100, w=72):
    p = i/(n if n else 100)
    s = "β–°" * int(round(p * w)) + "β–±" * int(round(((1-p) * w)))
    if i >= n:
        print("\033[0K", end="")
    else:
        print("\0337" + s + "\0338", end="")
    sys.stdout.flush()

Changes to dev/gh_conv.py.

12
13
14
15
16
17
18




19
20
21
22
23
24
25
        f.write(json.dumps(results, indent=4, ensure_ascii=False))

def read():
    with open("github.json", "r", encoding="utf-8") as f:
        return json.load(f)
        
def tree2dir(tree):




    tree = [p["path"] for p in tree]
    tree = [re.sub("\{\{\s*cookiecutter\.(\w+)\s*\}\}", "{{$\\1}}", p) for p in tree]
    ls = []
    pfx = ["    ", "    ", "    ", "    ", "    ", "    "]
    for p in tree.reverse() or tree:
        p = p.split("/")
        ind = len(p)







>
>
>
>







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
        f.write(json.dumps(results, indent=4, ensure_ascii=False))

def read():
    with open("github.json", "r", encoding="utf-8") as f:
        return json.load(f)
        
def tree2dir(tree):
    """
        convert dir/file/struct into β”œβ”€β”€ └── lists
        @todo: retain path names in ordered dict even? 
    """
    tree = [p["path"] for p in tree]
    tree = [re.sub("\{\{\s*cookiecutter\.(\w+)\s*\}\}", "{{$\\1}}", p) for p in tree]
    ls = []
    pfx = ["    ", "    ", "    ", "    ", "    ", "    "]
    for p in tree.reverse() or tree:
        p = p.split("/")
        ind = len(p)
48
49
50
51
52
53
54




55
56
57
58


59


60

61



62





63
64
65
66
67
68
69
70
71
72
73
74

75
76
77
78
79
80
81

82
83
84
85
86
87
88
    return r

# extract from tables like """  | `project_slug` | Description of flag... | """
def readme2cfgdesc(readme):
    return dict(re.findall("^\s*\|\s* `?(\w+)`? \s*\|\s* (\w.+) \s*\|$", readme, re.M|re.X))

def ccjson2cfg(kv, desc):




    # invalid ex: souravsingh/cookiecutter-bear
    _special = ["_extensions"]
    c = []
    for k,v in kv.items():


        if k.startswith("_") and not k in _special:


            continue

        if isinstance(v, list):



            _type = "select"





        elif isinstance(v, dict):
            _type = "dict"
        elif v == None:
            _type = "str"
            v = ""
        elif isinstance(v, int):
            _type = "int"
        else:
            _type = "str"
        c.append({
            "type": _type,
            "name": k,

            "value": v,
            "class": "control" if k in _special else "cookiecutter",
            "description": desc.get(k, ""),
        })
    return c

def lang2api(lang, name, ccjson_text):

    if not lang:
        lang = "other"
    api = re.sub("\s+", "-", lang.lower())
    if m := re.search("(django|flask|node|wordpress|mediawiki)", api):
        api = m[1]
    if m := re.search('"_api":\s*"([\w\-]+)"', ccjson_text):
        api = m[1]







>
>
>
>

|


>
>
|
>
>
|
>

>
>
>
|
>
>
>
>
>







<
<

<

>

|





>







52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90


91

92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
    return r

# extract from tables like """  | `project_slug` | Description of flag... | """
def readme2cfgdesc(readme):
    return dict(re.findall("^\s*\|\s* `?(\w+)`? \s*\|\s* (\w.+) \s*\|$", readme, re.M|re.X))

def ccjson2cfg(kv, desc):
    """
        transform key:value dict into pluginconf options structure,
        so we can differentiate types and add descriptions
    """
    # invalid ex: souravsingh/cookiecutter-bear
    _special = ["_extensions", "_copy_without_render"]
    c = []
    for k,v in kv.items():
        _class, _type = "cookiecutter", "str"
        # class
        if k.startswith("__"):
            _class = "private"
        elif k.startswith("_"):
            _class = "control"
        # types
        if isinstance(v, list):
            c.append({
                "name": k,
                "type": "select",
                "select": v,
                "value": v[0] if len(v) else "",
                "class": _class,
                "description": desc.get(k, ""),
            })
            continue
        elif isinstance(v, dict):
            _type = "dict"
        elif v == None:
            _type = "str"
            v = ""
        elif isinstance(v, int):
            _type = "int"


        c.append({

            "name": k,
            "type": _type,
            "value": v,
            "class": _class,
            "description": desc.get(k, ""),
        })
    return c

def lang2api(lang, name, ccjson_text):
    """ extract lang or api name from vnd/pkg name """
    if not lang:
        lang = "other"
    api = re.sub("\s+", "-", lang.lower())
    if m := re.search("(django|flask|node|wordpress|mediawiki)", api):
        api = m[1]
    if m := re.search('"_api":\s*"([\w\-]+)"', ccjson_text):
        api = m[1]

Changes to setup.py.

15
16
17
18
19
20
21

22
23
24
25
26
27
28
    name="cookiedough",
    long_description="README.md",
    packages=["cookiedough"],
    package_dir={"": "."},
    package_data={
        "cookiedough": [
            "./*.json",

            "./help/*.*",
        ],
    },
    include_package_data=True,
    entry_points={
        "console_scripts": [
            "cookiedough=cookiedough:main",







>







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    name="cookiedough",
    long_description="README.md",
    packages=["cookiedough"],
    package_dir={"": "."},
    package_data={
        "cookiedough": [
            "./*.json",
            "./*.patch",
            "./help/*.*",
        ],
    },
    include_package_data=True,
    entry_points={
        "console_scripts": [
            "cookiedough=cookiedough:main",