|
1 | 1 | import ast |
| 2 | +import sys |
| 3 | +from collections import defaultdict |
| 4 | +from dataclasses import dataclass, field |
| 5 | +from typing import List, Optional |
2 | 6 |
|
3 | 7 | import black |
4 | 8 | import requests |
5 | 9 | import typer |
6 | 10 |
|
7 | | -from codegen.util import make_literal |
8 | | - |
9 | 11 | app = typer.Typer(result_callback=lambda x: print(x)) |
10 | 12 |
|
11 | 13 |
|
| 14 | +def make_function( |
| 15 | + source_type: ast.expr, dest_type: ast.expr, taxon_id: bool, overload: bool = True |
| 16 | +) -> ast.FunctionDef: |
| 17 | + """ |
| 18 | + Makes a `submit()` function definition, as used by the ID mapper |
| 19 | +
|
| 20 | + Params: |
| 21 | + source_type: Type of the `source` argument |
| 22 | + dest_type: Type of the `dest` argument |
| 23 | + taxon_id: If true, include the `taxon_id` parameter |
| 24 | + overload: If true, this is a function overload |
| 25 | + """ |
| 26 | + args: List[ast.arg] = [ |
| 27 | + ast.arg( |
| 28 | + arg="cls", |
| 29 | + ), |
| 30 | + # source: Literal[...] |
| 31 | + ast.arg( |
| 32 | + arg="source", |
| 33 | + annotation=source_type, |
| 34 | + ), |
| 35 | + # source: dest[...] |
| 36 | + ast.arg( |
| 37 | + arg="dest", |
| 38 | + annotation=dest_type, |
| 39 | + ), |
| 40 | + # ids: Iterable[str] |
| 41 | + ast.arg( |
| 42 | + "ids", |
| 43 | + ast.Subscript(ast.Name("Iterable"), ast.Name("str")), |
| 44 | + ), |
| 45 | + ] |
| 46 | + defaults: list[Optional[ast.expr]] = [None, None, None] |
| 47 | + |
| 48 | + if taxon_id: |
| 49 | + # taxon_id: Optional[str] = None |
| 50 | + args.append( |
| 51 | + # taxon_id: bool |
| 52 | + ast.arg( |
| 53 | + "taxon_id", |
| 54 | + annotation=ast.Subscript(ast.Name("Optional"), ast.Name("int")), |
| 55 | + ) |
| 56 | + ) |
| 57 | + defaults.append(ast.Constant(None)) |
| 58 | + |
| 59 | + decorator_list: list[ast.expr] = [ |
| 60 | + ast.Name("classmethod"), |
| 61 | + ] |
| 62 | + if overload: |
| 63 | + decorator_list.append( |
| 64 | + ast.Name("overload"), |
| 65 | + ) |
| 66 | + |
| 67 | + return ast.FunctionDef( |
| 68 | + name=f"submit", |
| 69 | + args=ast.arguments( |
| 70 | + posonlyargs=[], args=args, kwonlyargs=[], kw_defaults=[], defaults=defaults # type: ignore |
| 71 | + ), |
| 72 | + body=[ast.Expr(ast.Constant(value=...))], |
| 73 | + decorator_list=decorator_list, |
| 74 | + ) |
| 75 | + |
| 76 | + |
| 77 | +@dataclass |
| 78 | +class Rule: |
| 79 | + """ |
| 80 | + Represents a "rule" in the Uniprot API terminology, which is a method overload |
| 81 | + in the Unipressed world. A rule is a set of allowed conversions from one database |
| 82 | + to another. |
| 83 | + """ |
| 84 | + |
| 85 | + #: Rule ID |
| 86 | + id: int = 0 |
| 87 | + #: List of databases that can be converted to, in this rule |
| 88 | + tos: list[ast.Constant] = field(default_factory=list) |
| 89 | + #: List of databases that can be converted from, in this rule |
| 90 | + froms: list[ast.Constant] = field(default_factory=list) |
| 91 | + #: Whether this rule supports specifying the taxon ID |
| 92 | + taxon_id: bool = False |
| 93 | + |
| 94 | + def to_function(self) -> ast.FunctionDef: |
| 95 | + return make_function( |
| 96 | + source_type=ast.Subscript( |
| 97 | + value=ast.Name("Literal"), |
| 98 | + slice=ast.Tuple(elts=self.froms), |
| 99 | + ), |
| 100 | + dest_type=ast.Subscript( |
| 101 | + value=ast.Name("Literal"), |
| 102 | + slice=ast.Tuple(elts=self.tos), # type: ignore |
| 103 | + ), |
| 104 | + taxon_id=self.taxon_id, |
| 105 | + overload=True, |
| 106 | + ) |
| 107 | + |
| 108 | + |
| 109 | +# ast.unparse uses Python 3.9 |
| 110 | +assert sys.version_info >= (3, 9) |
| 111 | + |
| 112 | + |
12 | 113 | @app.command() |
13 | | -def main(): |
14 | | - froms: list[ast.Constant] = [] |
15 | | - tos: list[ast.Constant] = [] |
| 114 | +def main() -> None: |
| 115 | + rules: defaultdict[int, Rule] = defaultdict(Rule) |
16 | 116 |
|
17 | | - for group in requests.get( |
| 117 | + # Build up a list of rules |
| 118 | + type_info = requests.get( |
18 | 119 | "https://rest.uniprot.org/configure/idmapping/fields" |
19 | | - ).json()["groups"]: |
20 | | - for item in group["items"]: |
| 120 | + ).json() |
| 121 | + for group_info in type_info["groups"]: |
| 122 | + for item in group_info["items"]: |
21 | 123 | if item["from"]: |
22 | | - froms.append(ast.Constant(item["name"])) |
23 | | - if item["to"]: |
24 | | - tos.append(ast.Constant(item["name"])) |
| 124 | + rules[item["ruleId"]].froms.append(ast.Constant(item["name"])) |
| 125 | + for rule_info in type_info["rules"]: |
| 126 | + rule = rules[rule_info["ruleId"]] |
| 127 | + for to in rule_info["tos"]: |
| 128 | + rule.tos.append(ast.Constant(to)) |
| 129 | + rule.taxon_id = rule_info["taxonId"] |
| 130 | + rule.id = rule_info["ruleId"] |
25 | 131 |
|
| 132 | + # Create a class that has one method overload per rule |
26 | 133 | module = ast.Module( |
27 | 134 | body=[ |
28 | 135 | ast.ImportFrom( |
29 | 136 | module="typing_extensions", |
30 | 137 | names=[ |
31 | 138 | ast.alias("Literal"), |
32 | | - ast.alias("TypeAlias"), |
| 139 | + ast.alias("overload"), |
33 | 140 | ], |
34 | 141 | level=0, |
35 | 142 | ), |
36 | | - make_literal(ast.Name("From"), froms), |
37 | | - make_literal(ast.Name("To"), tos), |
| 143 | + ast.ImportFrom( |
| 144 | + module="typing", |
| 145 | + names=[ |
| 146 | + ast.alias("Iterable"), |
| 147 | + ast.alias("Optional"), |
| 148 | + ], |
| 149 | + level=0, |
| 150 | + ), |
| 151 | + ast.ClassDef( |
| 152 | + name="SubmitDummyClass", |
| 153 | + body=[ |
| 154 | + *[rule.to_function() for rule in rules.values()], |
| 155 | + make_function( |
| 156 | + source_type=ast.Name("str"), |
| 157 | + dest_type=ast.Name("str"), |
| 158 | + taxon_id=True, |
| 159 | + overload=False, |
| 160 | + ), |
| 161 | + ], |
| 162 | + decorator_list=[], |
| 163 | + bases=[], |
| 164 | + keywords=[], |
| 165 | + ), |
38 | 166 | ], |
39 | 167 | type_ignores=[], |
40 | 168 | ) |
41 | 169 |
|
| 170 | + # Produce the formatted output |
42 | 171 | print( |
43 | 172 | black.format_file_contents( |
44 | 173 | ast.unparse(ast.fix_missing_locations(module)), |
|
0 commit comments