diff --git a/apps/_dashboard/__init__.py b/apps/_dashboard/__init__.py index 4c25f2924..c0f9a0fe8 100644 --- a/apps/_dashboard/__init__.py +++ b/apps/_dashboard/__init__.py @@ -28,6 +28,8 @@ ) from py4web.core import DAL, Fixture, Reloader, Session, dumps, error_logger, safely from py4web.utils.factories import ActionFactory +from py4web.utils.grid import Grid +from yatl.helpers import A from .diff2kryten import diff2kryten from .utils import * @@ -156,10 +158,51 @@ def logout(): session["user"] = None return dict() - @action("dbadmin") + @action("tickets/search") @action.uses(Logged(session), "dbadmin.html") def dbadmin(): - return dict(languages=dumps(getattr(T.local, "language", {}))) + db = error_logger.database_logger.db + + def make_grid(): + make_safe(db) + table = db.py4web_error + columns = [field for field in table if not field.name == "snapshot"] + return Grid(table, columns=columns) + + grid = action.uses(db)(make_grid)() + return dict(grid=grid) + + @action("dbadmin///") + @action.uses(Logged(session), "dbadmin.html") + def dbadmin(app_name, db_name, table_name): + module = Reloader.MODULES.get(app_name) + db = getattr(module, db_name) + + def make_grid(): + make_safe(db) + table = db[table_name] + for field in table: + field.readable = True + field.writable = True + columns = [ + field + for field in table + if field.type + in ( + "id", + "string", + "integer", + "double", + "time", + "date", + "datetime", + "boolean", + ) + ] + return Grid(table, columns=columns) + + grid = action.uses(db)(make_grid)() + return dict(grid=grid) @action("info") @session_secured @@ -350,69 +393,25 @@ def api(path): if not module: raise HTTP(404) - - def url(*args): - return request.url + "/" + "/".join(args) - databases = [ name for name in dir(module) if isinstance(getattr(module, name), DAL) ] - if len(args) == 1: - - def tables(name): - db = getattr(module, name) - make_safe(db) - return [ - { - "name": t._tablename, - "fields": t.fields, - "link": url(name, t._tablename) + "?model=true", - } - for t in getattr(module, name) - ] - - return { - "databases": [ - {"name": name, "tables": tables(name)} for name in databases - ] - } - elif len(args) > 2 and args[1] in databases: - db = getattr(module, args[1]) + + def tables(name): + db = getattr(module, name) make_safe(db) - id = args[3] if len(args) == 4 else None - policy = Policy() - for table in db: - policy.set( - table._tablename, - "GET", - authorize=True, - allowed_patterns=["**"], - allow_lookup=True, - fields=table.fields, - ) - policy.set(table._tablename, "PUT", authorize=True, fields=table.fields) - policy.set( - table._tablename, "POST", authorize=True, fields=table.fields - ) - policy.set(table._tablename, "DELETE", authorize=True) - - def make_writable(tablename): - if tablename in db: - for field in db[tablename]: - field.writable = True - - # must wrap into action uses to make sure it closes transactions - data = action.uses(db)( - lambda: make_writable(args[2]) - or RestAPI(db, policy)( - request.method, args[2], id, request.query, request.json - ) - )() - else: - data = {} - if "code" in data: - response.status = data["code"] - return data + return [ + { + "name": t._tablename, + "fields": t.fields, + "link": URL("dbadmin", app_name, name, t._tablename), + } + for t in getattr(module, name) + ] + + return { + "databases": [{"name": name, "tables": tables(name)} for name in databases] + } if MODE == "full": diff --git a/apps/_dashboard/static/components/mtable.html b/apps/_dashboard/static/components/mtable.html deleted file mode 100644 index 5a69f5455..000000000 --- a/apps/_dashboard/static/components/mtable.html +++ /dev/null @@ -1,117 +0,0 @@ -
- - - - - - - - - - - -
- - - - -
{{message}}
(no items found)(one item found)({{table.count}} items found)
-
- -
-
- - - - - - - - - -
{{field.label}} - - - - True - False - None - - - - - - - - - - - - - - - - -
{{errors[field.name]}}
-
- Referenced By - -
- {{name}} -
-
-
- - - -
-
-
- - - - - - - - - - - -
- {{field.label}} - - -
- - [{{item[field.name]}}] - - {{item[field.name]}} - (json) - {{item[field.name]}} - {{item[field.name]}} - {{item[field.name]}} - {{item[field.name]}} - {{item[field.name]}} - {{item[field.name]}} - {{JSON.stringify(item[field.name])}} - {{JSON.stringify(item[field.name])}} - {{(item[field.name]||'').replace('T','@').substr(0,16)}} - {{item[field.name]}} - - - - - - [{{item[field.name]}}] - -
- -
-
diff --git a/apps/_dashboard/static/components/mtable.js b/apps/_dashboard/static/components/mtable.js deleted file mode 100644 index dff68c0a8..000000000 --- a/apps/_dashboard/static/components/mtable.js +++ /dev/null @@ -1,220 +0,0 @@ -(function(){ - - var mtable = { props: ['url', 'filter', 'order', 'editable', 'create', 'deletable', 'render'], data: null, methods: {}}; - - mtable.data = function() { - var data = {url: this.url, - busy: false, - filter: this.filter || '', - order: this.order || '', - errors: {}, - item: null, - message: '', - reference_options: {}, - table: { model: [], items: [], count: 0}, - string_values: {} - }; - mtable.methods.load.call(data); - return data; - }; - - mtable.methods.toggle = function(obj, key) { - obj[key] = !obj[key]; - }; - mtable.methods.load = function() { - let self = this; - let length = this.table.items.length; - let url = this.url + '?@limit=20'; - if (length) url+='&@offset='+length; else url+='&@model=true'; - let filters = self.filter.split(' and ').filter((f)=>{return f.trim() != ''}); - filters = filters.filter((f)=>{return f.trim();}).map((f)=>{ - let parts = (f - .replace(/ equals? /,'==') - .replace('==','.eq=') - .replace('!=','.ne=') - .replace('<','.lt=') - .replace('>','.gt=') - .replace('<=','.le=') - .replace('>=','.ge=') - .replace(' in ','.in=') - .replace(' startswith ','~') - .replace('~','startswith=') - .split(/[=]/)); - let negate = false; - if (parts[0].substr(0,4) == 'not ') { - negate=true; - parts[0]=parts[0].substr(4); - } - return ((negate?'not.':'') + - parts[0].replace(/ /ig,'') + - '=' + parts[parts.length-1].replace(/^ /,'')); - }); - if (filters.length) url += '&'+filters.join('&'); - if (self.order) url += '&@order='+self.order; - self.busy = true; - Q.get(url).then(function (res) { - self.busy = false; - if(!length) self.table = res.json(); - else self.table.items = self.table.items.concat(res.json().items); - }); - }; - - mtable.methods.reorder = function (field) { - if (this.order == '~' + field.name) this.order = null; - else if (this.order == field.name) this.order = '~'+field.name; - else this.order = field.name; - this.table.items = []; - this.load(); - }; - - mtable.methods.clear = function() { - this.errors = {}; - this.item = null; - this.message = ''; - } - - mtable.methods.open_create = function () { - this.item = {}; - this.prepare_fields(this.item); - }; - - mtable.methods.open_edit = function (item) { - this.item = {}; - this.item = item; - this.prepare_fields(this.item); - }; - - mtable.methods.prepare_fields = function(item){ - let self = this; - for(var field of this.table.model){ - if(field.type == "list:string" || field.type == "list:integer" || field.type.substr(0,14) == "list:reference"){ - self.string_values[field.name] = JSON.stringify(item[field.name]); - } else if (field.type == "datetime") { - let value = item[field.name]; - if (!value) { - value = field.default != null ? field.default : ''; - value = value.split('.')[0]; - Vue.set(this.item, field.name, value); - } - } else if(field.type == "reference"){ - if (!(field.references in this.reference_options)){ - Vue.set(this.reference_options, field.references, []); - let reference_table_url = self.url.split('/'); - reference_table_url.pop() - reference_table_url.push(field.references) - reference_table_url = reference_table_url.join('/') + '?@options_list=true'; - console.log(reference_table_url); - Q.get(reference_table_url).then(function (res) { - let url_components = res.url.split('?')[0].split('/'); - self.reference_options[url_components[url_components.length - 1 ]] = res.json().items; - }); - - } - } - } - this.$nextTick(function(){ - Q("input[type=text].type-list-string,input[type=text].type-list-integer,input[type=text].type-list-reference").forEach( - function(elem){Q.tags_input(elem); - }); - }); - }; - - mtable.methods.parse_and_validate_json = function(event){ - try { - event.target.style.borderColor = ""; - return JSON.parse(event.target.value); - } - catch (error) { - event.target.style.borderColor = "#ff0000"; - } - } - - mtable.methods.trash = function (item) { - if (window.confirm("Really delete record?")) { - let url = this.url + '/' + item.id; - this.table.items = this.table.items.filter((i)=>{return i.id != item.id;}); - Q.delete(url); - if (item==this.item) this.item = null; - } - }; - - mtable.methods.save = function (item) { - let url = this.url; - self.busy = true; - for(var field of this.table.model) { - var is_list_integer = field.type == "list:integer" || field.type.substr(0,14) == "list:reference"; - if(field.type == "list:string" || is_list_integer) { - try { - item[field.name] = JSON.parse(this.string_values[field.name]); - } catch(err) { - alert("Invalid field value: " + field.name); - break; - } - } - } - if (item.id) { - url += '/' + item.id; - var data = JSON.parse(JSON.stringify(item)); - delete data["id"]; - Q.put(url, data).then(mtable.handle_response('put', this), - mtable.handle_response('put', this)); - } else { - Q.post(url, item).then(mtable.handle_response('post', this), - mtable.handle_response('post', this)); - } - }; - - mtable.handle_response = function(method, data) { - self.busy = false; - return function(res) { - res = res.json(); - if (method == 'post') { - data.table.items = []; - mtable.methods.load.call(data); - } - if (res.status == 'success') { - data.clear(); - location.reload(); - } else { - data.errors = res.errors; - data.message = res.message; - } - }; - }; - - mtable.methods.close = function () { - this.clear(); - }; - - mtable.methods.search = function () { - this.clear(); - this.table.items = []; - this.table.count = 0; - this.load(); - }; - - mtable.methods.go_ref_by = function(tablefield, item_id) { - tablefield = (tablefield+'.id').split('.'); - var tablename = tablefield[0], fieldname = tablefield[1]; - var source = window.location.search.substring(1); - var vars={}, items=source.split('&'); - items.map(function(item){ - var pair = item.split('='); - vars[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); - }); - vars['tablename'] = tablename - vars['filter'] = fieldname+'=='+item_id; - source = Object.keys(vars).map(function(key){ - return key+'='+encodeURIComponent(vars[key]); - }).join('&'); - window.location = window.location.href.split('?')[0]+'?'+source; - }; - - var scripts = document.getElementsByTagName('script'); - var src = scripts[scripts.length-1].src; - var path = src.substr(0, src.length-3) + '.html'; - Q.register_vue_component('mtable', path, function(template) { - mtable.template = template.data; - return mtable; - }); -})(); diff --git a/apps/_dashboard/static/css/no.css b/apps/_dashboard/static/css/no.css new file mode 100644 index 000000000..13dabcf41 --- /dev/null +++ b/apps/_dashboard/static/css/no.css @@ -0,0 +1,543 @@ +/***************************************************** + no.css version 2020-08-09.1 + + Designed to style pages without need for custom classes. + headers, paragraphs, buttons, tables, forms, + nav menus, alerts, and dialogs are styled automatically. + The only custom classes are color names, grid column sizes, + and a few convenience ones. + + Grid: + columns, col, c25, c33, c50, c66, c75 + Colors: + black, white, default, success, warning, error, info, transparent + Effects: + accordion, close, tags-list + Utils: + fill, padded + + License: MIT + *****************************************************/ + +/**************************************************** + global style + ****************************************************/ + +*, *:after, *:before { + border:0; + margin:0; + padding:0; + box-sizing: inherit; + color: inherit; +} + +html, body { + max-width: 100vw; + overflow-x: hidden; + box-sizing: border-box; +} + +body { + font-family: "Roboto", Helvetica, Arial, sans-serif; + line-height: 1.8em; + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr auto; +} + +/**************************************************** + elements style + ****************************************************/ + +p { + text-align:justify +} + +b, label, strong { + font-weight:bold +} + +ul { + list-style-type:none; + padding-left:20px +} + +a { + text-decoration:none; + color:#0074d9; + white-space:nowrap +} + +a:hover { + cursor:pointer +} + +h1,h2,h3,h4,h5,h6{ + font-weight:bold; + line-height: 1em; +} + +h1{ + font-size: 4em; + margin:1.0em 0 0.25em 0 +} + +h2{ + font-size: 2.4em; + margin:0.9em 0 0.25em 0 +} + +h3{ + font-size:1.8em; + margin:0.8em 0 0.25em 0 +} + +h4{ + font-size:1.6em; + margin:0.7em 0 0.30em 0 +} + +h5{ + font-size:1.4em; + margin:0.6em 0 0.40em 0 +} + +h6{ + font-size:1.2em; + margin:0.5em 0 0.50em 0 +} + +header, footer { + display:block; + width:100%; +} + +code { + background: #f4f5f6; + border-radius: .4rem; + font-size: 90; + margin: 0 .2rem; + padding: .2rem .5rem; + white-space: nowrap; +} + +p,li,button,fieldset,input,select,textarea,blockquote,table { + margin-bottom: 1.0rem; +} + +/**************************************************** + table + ****************************************************/ + +table { + border-collapse:collapse; + width: 100% +} + +tbody tr:hover { + background-color:#fbf6d9 +} + +thead tr { + background-color:#f1f1f1 +} + +tbody tr { + border-bottom:2px solid #f1f1f1 +} + +td, th { + padding: 4px 8px; + text-align: left; + vertical-align:top +} + +thead th { + vertical-align:bottom +} + +@media (min-width: 40rem) { + table { + display: table; + overflow-x: initial; + } +} + +/**************************************************** + buttons + ****************************************************/ + +[role="button"], button, input[type='button'], input[type='reset'], input[type='submit'] { + background-color: #0074d9; + border-radius: 5px; + margin-right: 10px; + margin-bottom: 10px; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: 1.1rem; + font-weight: 300; + height: 1.8rem; + line-height: 1.8rem; + padding: 0 1.0rem; + text-align: center; + text-decoration: none; + white-space: nowrap; + min-width: 100px; +} + +[role="button"]:focus, [role="button"]:hover, button:focus, button:hover, input[type='button']:focus, input[type='button']:hover, input[type='reset']:focus, input[type='reset']:hover, input[type='submit']:focus, input[type='submit']:hover { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); +} + +/**************************************************** + forms + ****************************************************/ + +input[type='color'], input[type='date'], input[type='datetime'], input[type='time'], input[type='datetime-local'], input[type='email'], input[type='month'], input[type='number'], input[type='password'], input[type='search'], input[type='tel'], input[type='text'], input[type='url'], input[type='week'], input:not([type]), textarea, select { + -webkit-appearance: none; + background-color: transparent; + border: 0.1rem solid #d1d1d1; + border-radius: 5px; + box-shadow: none; + box-sizing: inherit; + font-family: monospace; + font-size: 1.2em; + padding: .5em 1.0em .5em; + width: 100%; +} + +input[type='color']:focus, input[type='date']:focus, input[type='time']:focus, input[type='datetime']:focus, input[type='datetime-local']:focus, input[type='email']:focus, input[type='month']:focus, input[type='number']:focus, input[type='password']:focus, input[type='search']:focus, input[type='tel']:focus, input[type='text']:focus, input[type='url']:focus, input[type='week']:focus, input:not([type]):focus, textarea:focus, select:focus { + border-color: #0074d9; + outline: 0; +} + +select { + background: url('data:image/svg+xml;utf8,') center right no-repeat; +} + +select[multiple] { + background: none; + height: auto; +} + +textarea { + min-height: 6.5rem; +} + +fieldset { + border-width: 0; + padding: 0; +} + +input[type='checkbox'], input[type='radio'] { + display: inline; +} + +[disabled] { + cursor: default; + opacity: .5; +} + +/**************************************************** + grid and page formatting + ****************************************************/ + +body > center > * { + text-align: initial; + max-width: 900px; + margin-left: auto; + margin-right: auto; +} + +.columns { + display: table; + width: 100%; +} + +.columns .columns { + margin: 0 -1.5em; +} + +@media (min-width:600px) { + .col,.c25,.c33,.c50,.c66,.c75 { + padding: 1.5em; + display: table-cell; + vertical-align: top; + } + .c25 { width: 24.9%; } + .c33 { width: 33.3%; } + .c50 { width: 49.9%; } + .c66 { width: 66.6%; } + .c75 { width: 74.9%; } +} + +@media (max-width:600px) { + .col,.c25,.c33,.c50,.c66,.c75 { + padding: 20px; + display: block; + vertical-align: top; + } +} + +.columns:after { + content: ""; + clear: both; + display: table; +} + +/**************************************************** + colors + ****************************************************/ + +.transparent{background-color:transparent;color:#111} +.default{background-color:#0074d9;color:white} +.success{background-color:#2ecc40;color:white} +.warning{background-color:#ffdc00;color:#111} +.error{background-color:#cc1f00;color:white} +.info{background-color:#f1f1f1;color:#111} +.white{background-color:white;color:#111;padding 5px;} +.black{background-color:#111;color:white} + +/**************************************************** + navigation and nested menu + ****************************************************/ + +nav { + position:relative; + padding: 0 1.5em; + display: table; + width: 100vw; + height: 40px; +} + +nav ul { + list-style:none; + position:relative; + padding:0 +} + +nav > input[type=checkbox], nav > label { + display: none; +} + +@media (min-width:600px) { + nav > * { + display: table-cell; + vertical-align: middle; + } + nav > ul:last-child { + float:right; + } + nav > ul > li { + padding: 1.5em 0.5em + } +} + +@media (max-width:600px) { + nav > ul { + display: table-column; + vertical-align: middle; + } + nav > label { + position: absolute; + display: inline-block; + top: 5px; + right: 20px; + font-size: 2em; + } + nav > a { + display: inline-block; + padding-top: 8px !important; + } + nav > ul { + display: block; + } + nav > input[type=checkbox]:not(:checked) ~ ul { + display: none; + } + nav > ul > li { + display: block; + text-align: center; + padding: 0.5em 0.2em; + } +} + +nav li:hover { + background-color: #0074d9 +} + +nav li:hover > a { + color: white +} + +nav a { + padding:0 5px; + text-decoration:none; + text-align:left; + color: black; +} + +nav.black ul ul a { + color: black +} + +nav.black a, nav.black > label { + color: white +} + +nav li { + position:relative; + margin:0; + padding:0; + display: inline-block +} + +nav ul ul { + border:1px solid #e1e1e1; + visibility:hidden; + opacity:0; + position:absolute; + top:90%; + left:-20px; + padding:0; + z-index:1000; + transition:all 0.2s ease-out; + list-style-type: none; + box-shadow:5px 5px 10px #666; + background-color: white +} + +nav ul ul li { + width: 100%; +} + +nav ul ul a { + padding:10px 15px; + color:#333; + font-weight:700; + font-size:12px; + line-height:16px; + display: block; + color: #111; +} + +nav ul ul ul { + top:0; + left:80%; + z-index:1100 +} + +nav li:hover > ul { + visibility:visible; + opacity:1 +} + +nav>li>ul>li:first-child:before{ + content:''; + position:absolute; + width:1px; + height:1px; + border:10px solid transparent; + left:50px; + top:-20px; + margin-left:-10px; + border-bottom-color:white +} + +/**************************************************** + modal + ****************************************************/ + +[role="dialog"] > div { + position:fixed; + z-index:9999; + top:0; + bottom:0; + left:0; + right:0; + background-color:rgba(0,0,0,0.8); + padding-top:20vh; + transition:opacity 500ms; + visibility:hidden; + opacity:0; +} +[role="dialog"] > input[type=checkbox] { display: none !important; } +input[type=checkbox]:checked ~ div {visibility:visible; opacity:1} +[role="dialog"] > div > *:not(.close) {width: 66%; margin-left:auto; margin-right:auto; border-radius: 5px;} +[role="dialog"] > div > .close, [role="alert"] > .close { + background: url('data:image/svg+xml;utf8,') center right no-repeat; + width:24px; height:24px; cursor: pointer; position:absolute; top:15px; right:15px; + cursor: pointer; +} + +/**************************************************** + accordion + ****************************************************/ + +.accordion>label{cursor:pointer} +.accordion>input ~ label:before {content:"\25b2"; color:#ddd} +.accordion>input:checked ~ label:before {content:"\25bc"; color:#ddd} +.accordion>input {display:none} +.accordion>input:checked ~ *:not(label) { + max-height: 1000px !important; + overflow:hidden !important; + -webkit-transition: max-height .3s ease-in; + transition: max-height .3s ease-in; +} + +.accordion>*:not(label) { + max-height: 0; + overflow: hidden; + margin: 0; + padding: 0; + -webkit-transition: max-height .3s ease-out; + transition: max-height .3s ease-out; +} + +/**************************************************** + convenience + ****************************************************/ + +[role="alert"] { + margin: 1.5em; + padding: 1.5em; + position: relative; + border-radius: 5px; + color: black; +} + +[role="alert"] > .close { + position: absolute; + top: 10px; + right: 10px; +} + +.padded { + padding: 1.5em; +} + +.fill { + width: 100%; +} + +ul.tags-list { + padding-left: 0; +} + +ul.tags-list li { + display: inline-block; + border-radius: 100px; + background-color: #111111; + color: white; + padding: 0.3em 0.8em 0.2em 0.8em; + line-height: 1.2em; + margin: 2px; + cursor: pointer; + opacity: 0.2; + text-transform: capitalize; +} + +ul.tags-list li[data-selected=true] { + opacity: 1.0; +} diff --git a/apps/_dashboard/templates/dbadmin.html b/apps/_dashboard/templates/dbadmin.html index e501ec664..1fed8cf33 100644 --- a/apps/_dashboard/templates/dbadmin.html +++ b/apps/_dashboard/templates/dbadmin.html @@ -1,28 +1,2 @@ - - - - - - - -
- -
- -
-
- -
-
- - - - - - - - +[[extend "layout.html"]] +[[=grid]] diff --git a/apps/_dashboard/templates/index.html b/apps/_dashboard/templates/index.html index b989c083d..a86c366c0 100644 --- a/apps/_dashboard/templates/index.html +++ b/apps/_dashboard/templates/index.html @@ -124,7 +124,7 @@

- + {{name}} @@ -166,7 +166,7 @@

{{ticket.method}} {{ticket.path}} {{ticket.error}} - Search + Search diff --git a/apps/_dashboard/templates/layout.html b/apps/_dashboard/templates/layout.html new file mode 100644 index 000000000..e202a571a --- /dev/null +++ b/apps/_dashboard/templates/layout.html @@ -0,0 +1,56 @@ + + + + + + + + + + [[block page_head]][[end]] + + +
+ + +
+ +
+
+ + +
+
+ + [[include]] +
+
+ +
+

+ Made with py4web +

+
+ + + + [[block page_scripts]][[end]] + diff --git a/apps/_scaffold/templates/layout.html b/apps/_scaffold/templates/layout.html index 0edb16f51..8072d8107 100644 --- a/apps/_scaffold/templates/layout.html +++ b/apps/_scaffold/templates/layout.html @@ -10,7 +10,6 @@ .py4web-validation-error{margin-top:-16px; font-size:0.8em;color:red;} .grid-table-wrapper{overflow-x: auto;} - [[block page_head]][[end]]
@@ -18,36 +17,8 @@
@@ -70,5 +41,4 @@ - [[block page_scripts]][[end]] diff --git a/py4web/utils/form.py b/py4web/utils/form.py index db0e8efa3..ac3c66d22 100644 --- a/py4web/utils/form.py +++ b/py4web/utils/form.py @@ -422,7 +422,7 @@ def __call__( else: if field.name in self.widgets: widget = self.widgets[field.name] - elif field.type == "text": + elif field.type == "text" or field.type == "json": widget = TextareaWidget() elif field.type == "datetime": widget = DateTimeWidget() diff --git a/uv.lock b/uv.lock index eec9c11f9..fefa9264a 100644 --- a/uv.lock +++ b/uv.lock @@ -763,7 +763,7 @@ requires-dist = [ { name = "pluralize", specifier = ">=20240515.1" }, { name = "portalocker" }, { name = "pycryptodome" }, - { name = "pydal", specifier = ">=20250526.2" }, + { name = "pydal", specifier = ">=20250526.4" }, { name = "pyjwt", specifier = ">=2.0.1" }, { name = "pystemmer", marker = "extra == 'docs'", specifier = ">=2.2" }, { name = "pytest", marker = "extra == 'test'" }, @@ -837,11 +837,11 @@ wheels = [ [[package]] name = "pydal" -version = "20250526.2" +version = "20250526.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/cb/7d4891390b3ba80c767dc83f478aaba55bd0919a39c2acf0f2acc5d95b02/pydal-20250526.2.tar.gz", hash = "sha256:45380f40d88a457792853b5e539e7be5900a00f109704e64528ea2577eeb210c", size = 629318 } +sdist = { url = "https://files.pythonhosted.org/packages/6e/d4/234c3d78ce49947cf0937e7f06841b8cbe1ae2dabbce0ab005ff17d6b4cc/pydal-20250526.4.tar.gz", hash = "sha256:3a9af01dc17bbc7a6a74d51df6f01c1c9026888f264c9811e8644bc19be7f76f", size = 629691 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/aa/a93fd0661814449432f62b6bd721df4239a2a643a24c67d125bb78d4c623/pydal-20250526.2-py2.py3-none-any.whl", hash = "sha256:adff906ecb058f10d8a9c05640b2a76134f45841af4b9350788bcab65f92915e", size = 250436 }, + { url = "https://files.pythonhosted.org/packages/4f/2a/98f13d018da3fc30d70b2cbc094972f0832dde525698044a1199456f0682/pydal-20250526.4-py2.py3-none-any.whl", hash = "sha256:94570a7985eca46a6f9e1be12f88145fc6da0018a2cbedac1b0804304f6503d1", size = 250542 }, ] [[package]]