From c915501ca0e1a98369884fa34f0b452259160c6e Mon Sep 17 00:00:00 2001 From: David Roe Date: Thu, 14 Nov 2024 04:12:30 -0500 Subject: [PATCH 01/14] New page explaining methods for accessing large amounts of data --- lmfdb/api/api.py | 14 +++++- lmfdb/api/templates/database_options.html | 55 +++++++++++++++++++++++ lmfdb/homepage/sidebar.yaml | 2 +- 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 lmfdb/api/templates/database_options.html diff --git a/lmfdb/api/api.py b/lmfdb/api/api.py index fe8b12346f..734492741e 100644 --- a/lmfdb/api/api.py +++ b/lmfdb/api/api.py @@ -56,10 +56,22 @@ def get_database_info(show_hidden=False): info[database].append((table, table[i+1:], coll.count())) return info +@api_page.route("/options") +def options(): + return render_template( + "database_options.html", + title="Access options for the LMFDB database", + learnmore=[("API", url_for(".index")), + ("Table statistics", url_for(".stats")), + ("lmfdb-lite", "https://www.github.com/roed314/lmfdb-lite"), + ("Install the LMFDB locally", "https://github.com/LMFDB/lmfdb/blob/main/GettingStarted.md")], + bread=[("Access options", " ")], + ) + @api_page.route("/") def index(show_hidden=False): databases = get_database_info(show_hidden) - title = "Database" + title = "API" return render_template("api.html", **locals()) @api_page.route("/all") diff --git a/lmfdb/api/templates/database_options.html b/lmfdb/api/templates/database_options.html new file mode 100644 index 0000000000..c443ac1411 --- /dev/null +++ b/lmfdb/api/templates/database_options.html @@ -0,0 +1,55 @@ +{% extends 'homepage.html' %} + +{% block content %} + +
+

+ There are three options available to access large amounts of data from the LMFDB. +

+
+ +

Downloads from search result pages

+ +
+

Every search results page in the LMFDB has a download button between the search inputs and the results. This is the simplest option, offers several download formats and the columns included in the download match those displayed on the search results page. There are two main downsides. First, it is limited to roughly 100 MB per download and to a 30 seconds limit. Second, it is limited to queries expressible on search pages and the columns that are included in the search results, so if you are interested in quantities that are visible on object homepages but not shown on search result pages this method will not work.

+
+ +

The API

+ +
+

The LMFDB offers an API. This provides a http-based mechanism to query the data tables that underlie the LMFDB's webpages, providing access to all of the data used to create object homepages. You can construct queries programatically and send them to the LMFDB's webserver, so this method is available both interactively and from any programming language that can request webpages. There is still a rate limit: each request can fetch at most 100 results, and the overall limit is at most 10,000 results from a query. So if you want to access large numbers of results the API will be cumbersome. Finally, the possible queries are somewhat limited.

+

The API also serves as a way to understand the structure of the data tables underlying the LMFDB: it contains descriptions of the tables and their columns, as well as an overview of the sizes of the tables constituting the LMFDB.

+
+ +

Direct SQL connection

+ +
+

You can open a direct connection to a read-only mirror of the LMFDB's SQL database, using the access information below. You can connect via standard tools for working with Postgres databases, by using the lmfdb-lite python module, or by installing the LMFDB locally. The first requires writing your own SQL queries, while the later two require learning the LMFDB's custom query language. However, this option provides the most flexibility: there are no timeouts or rate limits, and you can run arbitrary queries.

+
+ +

SQL Mirror Access

+ + + + + + + + + + + + + + + + + + + + + + +
hostdevmirror.lmfdb.xyz
port5432
dbnamelmfdb
userlmfdb
passwordlmfdb
+ +{% endblock %} diff --git a/lmfdb/homepage/sidebar.yaml b/lmfdb/homepage/sidebar.yaml index 7dc1e1a4d8..882b337882 100644 --- a/lmfdb/homepage/sidebar.yaml +++ b/lmfdb/homepage/sidebar.yaml @@ -227,7 +227,7 @@ type: single heading: title: Database - url_for: "API.index" + url_for: "API.options" type: knowl ... From 466bf8966af9f8e4297dc0c65788033837fce669 Mon Sep 17 00:00:00 2001 From: David Roe Date: Fri, 15 Nov 2024 21:28:48 -0500 Subject: [PATCH 02/14] Use knowls --- lmfdb/api/templates/database_options.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lmfdb/api/templates/database_options.html b/lmfdb/api/templates/database_options.html index c443ac1411..127ea36344 100644 --- a/lmfdb/api/templates/database_options.html +++ b/lmfdb/api/templates/database_options.html @@ -11,20 +11,19 @@

Downloads from search result pages

-

Every search results page in the LMFDB has a download button between the search inputs and the results. This is the simplest option, offers several download formats and the columns included in the download match those displayed on the search results page. There are two main downsides. First, it is limited to roughly 100 MB per download and to a 30 seconds limit. Second, it is limited to queries expressible on search pages and the columns that are included in the search results, so if you are interested in quantities that are visible on object homepages but not shown on search result pages this method will not work.

+ {{KNOWL_INC('intro.download_search')}}

The API

-

The LMFDB offers an API. This provides a http-based mechanism to query the data tables that underlie the LMFDB's webpages, providing access to all of the data used to create object homepages. You can construct queries programatically and send them to the LMFDB's webserver, so this method is available both interactively and from any programming language that can request webpages. There is still a rate limit: each request can fetch at most 100 results, and the overall limit is at most 10,000 results from a query. So if you want to access large numbers of results the API will be cumbersome. Finally, the possible queries are somewhat limited.

-

The API also serves as a way to understand the structure of the data tables underlying the LMFDB: it contains descriptions of the tables and their columns, as well as an overview of the sizes of the tables constituting the LMFDB.

+ {{KNOWL_INC('intro.api')}}

Direct SQL connection

-

You can open a direct connection to a read-only mirror of the LMFDB's SQL database, using the access information below. You can connect via standard tools for working with Postgres databases, by using the lmfdb-lite python module, or by installing the LMFDB locally. The first requires writing your own SQL queries, while the later two require learning the LMFDB's custom query language. However, this option provides the most flexibility: there are no timeouts or rate limits, and you can run arbitrary queries.

+ {{KNOWL_INC('intro.direct_sql')}}

SQL Mirror Access

From cbf26fe150a8375f07463dec61a9d95431a1c02a Mon Sep 17 00:00:00 2001 From: David Roe Date: Fri, 15 Nov 2024 23:26:38 -0500 Subject: [PATCH 03/14] Additional search options for number fields --- lmfdb/galois_groups/transitive_group.py | 75 +++++++++++++++++++++++++ lmfdb/number_fields/number_field.py | 65 +++++++++++++++++---- 2 files changed, 129 insertions(+), 11 deletions(-) diff --git a/lmfdb/galois_groups/transitive_group.py b/lmfdb/galois_groups/transitive_group.py index eb44485c42..60d93163ac 100644 --- a/lmfdb/galois_groups/transitive_group.py +++ b/lmfdb/galois_groups/transitive_group.py @@ -936,3 +936,78 @@ def get_aliases(): aliases[ky].append(nt) aliases[ky].sort() return aliases + +# These dictionaries are used by number field parsing code when user requests a dihedral galois group +dihedral_gal = { + 2: "2T1", + 4: "4T2", + 6: "6T2", + 8: "8T4", + 10: "10T2", + 12: "12T3", + 14: "14T2", + 16: "16T13", + 18: "18T5", + 20: "20T4", + 22: "22T2", + 24: "24T13", + 26: "26T2", + 28: "28T4", + 30: "30T3", + 32: "32T31", + 34: "34T2", + 36: "36T10", + 38: "38T2", + 40: "40T12", + 42: "42T5", + 44: "44T4", + 46: "46T2", +} + +dihedral_ngal = { + 3: "3T2", + 4: "4T3", + 5: "5T2", + 6: "6T3", + 7: "7T2", + 8: "8T6", + 9: "9T3", + 10: "10T3", + 11: "11T2", + 12: "12T12", + 13: "13T2", + 14: "14T3", + 15: "15T2", + 16: "16T56", + 17: "17T2", + 18: "18T13", + 19: "19T2", + 20: "20T10", + 21: "21T5", + 22: "22T3", + 23: "23T2", + 24: "24T34", + 25: "25T4", + 26: "26T3", + 27: "27T8", + 28: "28T10", + 29: "29T2", + 30: "30T14", + 31: "31T2", + 32: "32T374", + 33: "33T3", + 34: "34T3", + 35: "35T4", + 36: "36T47", + 37: "37T2", + 38: "38T3", + 39: "39T4", + 40: "40T46", + 41: "41T2", + 42: "42T11", + 43: "43T2", + 44: "44T9", + 45: "45T4", + 46: "46T3", + 47: "47T2", +} diff --git a/lmfdb/number_fields/number_field.py b/lmfdb/number_fields/number_field.py index 227828b1ec..9a102136c2 100644 --- a/lmfdb/number_fields/number_field.py +++ b/lmfdb/number_fields/number_field.py @@ -10,11 +10,11 @@ from lmfdb.utils import ( web_latex, to_dict, coeff_to_poly, comma, format_percentage, flash_error, display_knowl, CountBox, Downloader, prop_int_pretty, - SearchArray, TextBox, YesNoBox, YesNoMaybeBox, SubsetNoExcludeBox, + SearchArray, TextBox, YesNoBox, YesNoMaybeBox, SubsetNoExcludeBox, SelectBox, SubsetBox, TextBoxWithSelect, parse_bool_unknown, parse_posints, clean_input, nf_string_to_label, parse_galgrp, parse_ints, parse_bool, parse_signed_ints, parse_primes, parse_bracketed_posints, parse_nf_string, - parse_floats, parse_subfield, search_wrap, parse_padicfields, + parse_floats, parse_subfield, search_wrap, parse_padicfields, integer_options, raw_typeset, raw_typeset_poly, flash_info, input_string_to_poly, raw_typeset_int, compress_poly_Q, compress_polynomial) from lmfdb.utils.web_display import compress_int @@ -25,7 +25,8 @@ cclasses_display_knowl,character_table_display_knowl, group_phrase, galois_group_data, transitive_group_display_knowl, group_cclasses_knowl_guts, group_pretty_and_nTj, knowl_cache, - group_character_table_knowl_guts, group_alias_table) + group_character_table_knowl_guts, group_alias_table, + dihedral_gal, dihedral_ngal) from lmfdb.number_fields import nf_page, nf_logger from lmfdb.number_fields.web_number_field import ( field_pretty, WebNumberField, nf_knowl_guts, factor_base_factor, @@ -898,7 +899,39 @@ def number_field_search(info, query): parse_posints(info,query,'relative_class_number') parse_ints(info,query,'num_ram') parse_bool(info,query,'cm_field',qfield='cm') - parse_bool(info,query,'is_galois') + fi = info.get("field_is") + if fi == "cyc": + query["gal_is_cyclic"] = True + elif fi == "ab": + query["gal_is_abelian"] = True + elif fi in ["dih_ngal", "dih_gal"]: + opts = dihedral_gal if fi == "dih_gal" else dihedral_ngal + if "degree" in info: + opts = {n: opts[n] for n in integer_options(info["degree"], contained_in=list(opts))} + if "galois_label" in query: + # Added by parse_galgrp, so we intersect with opts + if isinstance(query["galois_label"], dict): + ggopt = set(query["galois_label"]["$in"]) + else: + ggopt = {query["galois_label"]} + opts = {n: gg for (n, gg) in opts.items() if gg in ggopt} + if len(opts) == 0: + # Incompatible with specified degree or galois labels, so we add an impossible condition + query["degree"] = -1 + elif len(opts) == 1: + n, gg = list(opts.items())[0] + query["degree"] = n + query["galois_label"] = gg + else: + query["degree"] = {"$in": list(opts)} + query["galois_label"] = list(opts.values()) + elif fi == "gal": + query["is_galois"] = True + elif fi == "solv": + query["gal_is_solvable"] = True + elif fi == "nsolv": + query["gal_is_solvable"] = False + parse_bracketed_posints(info,query,'class_group',check_divisibility='increasing',process=int) parse_primes(info,query,'ur_primes',name='Unramified primes', qfield='ramps',mode='exclude') @@ -1176,10 +1209,20 @@ def __init__(self): knowl="nf.galois_search", example="C5", example_span="[8,3], 8.3, C5 or 7T2") - is_galois = YesNoBox( - name="is_galois", - label="Galois", - knowl="nf.galois_group") + field_is_opts = [ + ("", ""), + ("cyc", "cyclic"), + ("ab", "abelian"), + ("dih_ngal", "dihedral non-Galois"), + ("dih_gal", "dihedral Galois"), + ("gal", "Galois"), + ("solv", "solvable"), + ("nsolv", "nonsolvable"),] + field_is = SelectBox( + name="field_is", + label="Field is", + knowl="nf.field_is", + options=field_is_opts) regulator = TextBox( name="regulator", label="Regulator", @@ -1259,7 +1302,7 @@ def __init__(self): self.browse_array = [ [degree, signature], [discriminant, rd], - [gal, is_galois], + [gal, field_is], [num_ram, grd], [class_number, class_group], [ram_primes, ur_primes], @@ -1271,13 +1314,13 @@ def __init__(self): self.refine_array = [ [degree, signature, num_ram, ram_primes, ur_primes ], - [gal, is_galois, subfield, class_group, class_number], + [gal, field_is, subfield, class_group, class_number], [discriminant, rd, grd, cm_field, relative_class_number], [regulator, completion, monogenic, index, inessentialprimes], [is_minimal_sibling]] #[degree, signature, class_number, class_group, cm_field], - #[num_ram, ram_primes, ur_primes, gal, is_galois], + #[num_ram, ram_primes, ur_primes, gal, field_is], #[discriminant, rd, grd, regulator, subfield], #[completion, is_minimal_sibling, monogenic, index, inessentialprimes], #[relative_class_number]] From 88edde491f46d648661a2958d07e2cad4c698e20 Mon Sep 17 00:00:00 2001 From: David Roe Date: Fri, 15 Nov 2024 23:30:07 -0500 Subject: [PATCH 04/14] Fix bug in knowl is_locked --- lmfdb/knowledge/knowl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lmfdb/knowledge/knowl.py b/lmfdb/knowledge/knowl.py index 0222ed58db..00cd6c2450 100644 --- a/lmfdb/knowledge/knowl.py +++ b/lmfdb/knowledge/knowl.py @@ -738,7 +738,7 @@ def is_locked(self, knowlid, delta_min=10): tdelta = timedelta(minutes=delta_min) time = now - tdelta selecter = SQL("SELECT username, timestamp FROM kwl_locks WHERE id = %s AND timestamp >= %s LIMIT 1") - L = self._execute(selecter, (knowlid, time)) + L = list(self._execute(selecter, (knowlid, time))) if L: return dict(zip(["username", "timestamp"], L[0])) From 3fbe7c584e211e1c4de0ac4cc7bb0e3ac34582dc Mon Sep 17 00:00:00 2001 From: David Roe Date: Fri, 15 Nov 2024 23:34:44 -0500 Subject: [PATCH 05/14] Better fix --- lmfdb/knowledge/knowl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lmfdb/knowledge/knowl.py b/lmfdb/knowledge/knowl.py index 00cd6c2450..85c8b782ec 100644 --- a/lmfdb/knowledge/knowl.py +++ b/lmfdb/knowledge/knowl.py @@ -738,7 +738,7 @@ def is_locked(self, knowlid, delta_min=10): tdelta = timedelta(minutes=delta_min) time = now - tdelta selecter = SQL("SELECT username, timestamp FROM kwl_locks WHERE id = %s AND timestamp >= %s LIMIT 1") - L = list(self._execute(selecter, (knowlid, time))) + L = self._safe_execute(selecter, (knowlid, time)) if L: return dict(zip(["username", "timestamp"], L[0])) From a09e40f9bf3966db5d2abe95ff9bce6c579933c3 Mon Sep 17 00:00:00 2001 From: David Roe Date: Sat, 16 Nov 2024 01:16:09 -0500 Subject: [PATCH 06/14] Fix bug --- lmfdb/number_fields/number_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lmfdb/number_fields/number_field.py b/lmfdb/number_fields/number_field.py index 9a102136c2..e035f056f2 100644 --- a/lmfdb/number_fields/number_field.py +++ b/lmfdb/number_fields/number_field.py @@ -924,7 +924,7 @@ def number_field_search(info, query): query["galois_label"] = gg else: query["degree"] = {"$in": list(opts)} - query["galois_label"] = list(opts.values()) + query["galois_label"] = {"$in": list(opts.values())} elif fi == "gal": query["is_galois"] = True elif fi == "solv": From 82e776bef2d34bd863c09620b6c25b1573135085 Mon Sep 17 00:00:00 2001 From: David Roe Date: Sat, 16 Nov 2024 01:37:02 -0500 Subject: [PATCH 07/14] Fix error in knowledge/review_recent --- lmfdb/knowledge/knowl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lmfdb/knowledge/knowl.py b/lmfdb/knowledge/knowl.py index 0222ed58db..b9c51c3c9a 100644 --- a/lmfdb/knowledge/knowl.py +++ b/lmfdb/knowledge/knowl.py @@ -430,7 +430,7 @@ def needs_review(self, days): tdelta = timedelta(days=days) time = now - tdelta fields = ['id'] + self._default_fields - selecter = SQL("SELECT {0} FROM (SELECT DISTINCT ON (id) {0} FROM kwl_knowls WHERE timestamp >= %s AND status >= %s AND (type = 1 OR type = -1) ORDER BY id, timestamp DESC) knowls WHERE status = 0 ORDER BY timestamp DESC").format(SQL(", ").join(map(Identifier, fields))) + selecter = SQL("SELECT {0} FROM (SELECT DISTINCT ON (id) {0} FROM kwl_knowls WHERE timestamp >= %s AND status >= %s AND type >= -1 AND type <= 1 ORDER BY id, timestamp DESC) knowls WHERE status = 0 ORDER BY timestamp DESC").format(SQL(", ").join(map(Identifier, fields))) L = self._safe_execute(selecter, [time, 0]) knowls = [Knowl(rec[0], data=dict(zip(fields, rec))) for rec in L] From e3516413c938329e17f8995371c5308fff92bf19 Mon Sep 17 00:00:00 2001 From: David Roe Date: Sat, 16 Nov 2024 03:57:09 -0500 Subject: [PATCH 08/14] Add mwgens to ec download --- lmfdb/elliptic_curves/elliptic_curve.py | 37 +++++++++++++++---------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/lmfdb/elliptic_curves/elliptic_curve.py b/lmfdb/elliptic_curves/elliptic_curve.py index 419f14b506..49360e3407 100644 --- a/lmfdb/elliptic_curves/elliptic_curve.py +++ b/lmfdb/elliptic_curves/elliptic_curve.py @@ -398,21 +398,6 @@ def url_for_label(label): modm_no_negative = r'(\d+)\.(\d+)\.(\d+)-(\d+)\.([a-z]+)\.(\d+)\.(\d+)' modm_image_label_regex = re.compile(modm_full + "|" + modm_not_computed + "|" + modm_no_negative) -class EC_download(Downloader): - table = db.ec_curvedata - title = "Elliptic curves" - inclusions = { - "curve": ( - ["ainvs"], - { - "sage": 'curve = EllipticCurve(out["ainvs"])', - "magma": 'curve := EllipticCurve(out`ainvs);', - "gp": 'curve = ellinit(mapget(out, "ainvs"));', - "oscar": 'curve = EllipticCurve(out["ainvs"])', - } - ) - } - def ec_postprocess(res, info, query): labels = [rec["lmfdb_label"] for rec in res] mwgens = {rec["lmfdb_label"]: rec["gens"] for rec in db.ec_mwbsd.search({"lmfdb_label":{"$in":labels}}, ["lmfdb_label", "gens"])} @@ -493,6 +478,28 @@ def modify_query(self, info, query): if info.get("optimal") == "on": query["__one_per__"] = "lmfdb_iso" + inclusions = { + "curve": ( + ["ainvs"], + { + "sage": 'curve = EllipticCurve(out["ainvs"])', + "magma": 'curve := EllipticCurve(out`ainvs);', + "gp": 'curve = ellinit(mapget(out, "ainvs"));', + "oscar": 'curve = EllipticCurve(out["ainvs"])', + } + ) + } + + def postprocess(self, row, info, query): + # Unfortunately, I don't see a good way to batch these database calls + # given how the download iterator works + if "mwgens" in info.get("showcol", "").split("."): + gens = db.ec_mwbsd.lucky({"lmfdb_label": row["lmfdb_label"]}, "gens") + if gens is not None: + gens = [(ZZ(a)/c, ZZ(b)/c) for (a,b,c) in gens] + row["mwgens"] = gens + return row + @search_wrap(table=db.ec_curvedata, title='Elliptic curve search results', err_title='Elliptic curve search input error', From bb2c2a1b8763237310265e98f0c53b7d24a91309 Mon Sep 17 00:00:00 2001 From: David Roe Date: Sat, 16 Nov 2024 12:51:58 -0500 Subject: [PATCH 09/14] Fix bug, add multiquadratic --- lmfdb/galois_groups/transitive_group.py | 8 ++++++++ lmfdb/number_fields/number_field.py | 14 ++++++++++---- lmfdb/utils/search_parsing.py | 20 ++++++++++++++------ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/lmfdb/galois_groups/transitive_group.py b/lmfdb/galois_groups/transitive_group.py index 60d93163ac..f4b97539d1 100644 --- a/lmfdb/galois_groups/transitive_group.py +++ b/lmfdb/galois_groups/transitive_group.py @@ -1011,3 +1011,11 @@ def get_aliases(): 46: "46T3", 47: "47T2", } + +multiquad = { + 2: "2T1", + 4: "4T2", + 8: "8T3", + 16: "16T4", + 32: "32T39", +} diff --git a/lmfdb/number_fields/number_field.py b/lmfdb/number_fields/number_field.py index e035f056f2..bc12cd3940 100644 --- a/lmfdb/number_fields/number_field.py +++ b/lmfdb/number_fields/number_field.py @@ -26,7 +26,7 @@ group_phrase, galois_group_data, transitive_group_display_knowl, group_cclasses_knowl_guts, group_pretty_and_nTj, knowl_cache, group_character_table_knowl_guts, group_alias_table, - dihedral_gal, dihedral_ngal) + dihedral_gal, dihedral_ngal, multiquad) from lmfdb.number_fields import nf_page, nf_logger from lmfdb.number_fields.web_number_field import ( field_pretty, WebNumberField, nf_knowl_guts, factor_base_factor, @@ -904,10 +904,15 @@ def number_field_search(info, query): query["gal_is_cyclic"] = True elif fi == "ab": query["gal_is_abelian"] = True - elif fi in ["dih_ngal", "dih_gal"]: - opts = dihedral_gal if fi == "dih_gal" else dihedral_ngal + elif fi in ["dih_ngal", "dih_gal", "multi_quad"]: + if fi == "dih_ngal": + opts = dihedral_ngal + elif fi == "dih_gal": + opts = dihedral_gal + else: + opts = multiquad if "degree" in info: - opts = {n: opts[n] for n in integer_options(info["degree"], contained_in=list(opts))} + opts = {n: opts[n] for n in integer_options(info["degree"], contained_in=list(opts), lower_bound=1, upper_bound=47) if n in opts} if "galois_label" in query: # Added by parse_galgrp, so we intersect with opts if isinstance(query["galois_label"], dict): @@ -1213,6 +1218,7 @@ def __init__(self): ("", ""), ("cyc", "cyclic"), ("ab", "abelian"), + ("multi_quad", "multi-quadratic"), ("dih_ngal", "dihedral non-Galois"), ("dih_gal", "dihedral Galois"), ("gal", "Galois"), diff --git a/lmfdb/utils/search_parsing.py b/lmfdb/utils/search_parsing.py index 4fd71b5674..020e818349 100644 --- a/lmfdb/utils/search_parsing.py +++ b/lmfdb/utils/search_parsing.py @@ -380,20 +380,28 @@ def parse_range2rat(arg, key, process): # We parse into a list of singletons and pairs, like [[-5,-2], 10, 11, [16,100]] # If split0, we split ranges [-a,b] that cross 0 into [-a, -1], [1, b] -def parse_range3(arg, split0=False): +def parse_range3(arg, split0=False, lower_bound=None, upper_bound=None): if isinstance(arg, str): arg = arg.replace(" ", "") if "," in arg: - return sum([parse_range3(a, split0) for a in arg.split(",")], []) + return sum([parse_range3(a, split0, lower_bound, upper_bound) for a in arg.split(",")], []) elif "-" in arg[1:]: ix = arg.index("-", 1) start, end = arg[:ix], arg[ix + 1:] if start: low = ZZ(str(start)) + if lower_bound is not None: + low = max(low, lower_bound) + elif lower_bound is not None: + low = lower_bound else: raise SearchParsingError("It needs to be an integer (such as 25), a range of integers (such as 2-10 or 2..10), or a comma-separated list of these (such as 4,9,16 or 4-25, 81-121).") if end: high = ZZ(str(end)) + if upper_bound is not None: + high = min(high, upper_bound) + elif upper_bound is not None: + high = upper_bound else: raise SearchParsingError("It needs to be an integer (such as 25), a range of integers (such as 2-10 or 2..10), or a comma-separated list of these (such as 4,9,16 or 4-25, 81-121).") if low == high: @@ -413,7 +421,7 @@ def parse_range3(arg, split0=False): else: return [ZZ(str(arg))] -def integer_options(arg, max_opts=None, contained_in=None): +def integer_options(arg, max_opts=None, contained_in=None, lower_bound=None, upper_bound=None): if not LIST_RE.match(arg) and MULT_PARSE.fullmatch(arg): # Make input work using some arithmetic expressions try: @@ -421,7 +429,7 @@ def integer_options(arg, max_opts=None, contained_in=None): arg = str(int(PowMulNodeVisitor().visit(ast_expression).body)) except (TypeError, ValueError, SyntaxError): raise SearchParsingError("Unable to evaluate expression.") - intervals = parse_range3(arg) + intervals = parse_range3(arg, lower_bound=lower_bound, upper_bound=upper_bound) check = max_opts is not None and contained_in is None if check and len(intervals) > max_opts: raise ValueError("Too many options.") @@ -698,12 +706,12 @@ def parse_not_element_of(inp, query, qfield, parse_singleton=int): # Parses signed ints as an int and a sign the fields these are stored are passed in as qfield = (sign_field, abs_field) # see SearchParser.__call__ for actual arguments when calling @search_parser(clean_info=True, prep_ranges=True) -def parse_signed_ints(inp, query, qfield, parse_one=None): +def parse_signed_ints(inp, query, qfield, parse_one=None, lower_bound=None, upper_bound=None): if parse_one is None: def parse_one(x): return (int(x.sign()), int(x.abs())) if x != 0 else (1, 0) sign_field, abs_field = qfield if SIGNED_LIST_RE.match(inp): - parsed = parse_range3(inp, split0=True) + parsed = parse_range3(inp, split0=True, lower_bound=lower_bound, upper_bound=upper_bound) # if there is only one part, we don't need an $or if len(parsed) == 1: parsed = parsed[0] From 03d61a22cb67fa583146a5aa3243a0f42ec18a49 Mon Sep 17 00:00:00 2001 From: John Voight Date: Sun, 17 Nov 2024 14:08:51 +1100 Subject: [PATCH 10/14] Update workshops.html --- lmfdb/templates/workshops.html | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/lmfdb/templates/workshops.html b/lmfdb/templates/workshops.html index 096c0dfc82..1ff2362a17 100644 --- a/lmfdb/templates/workshops.html +++ b/lmfdb/templates/workshops.html @@ -6,6 +6,75 @@