图片解析应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

317 lines
7.4 KiB

  1. def tags(*t):
  2. """
  3. Indicate that the values should be matched to a tag field
  4. ### Parameters
  5. - **t**: Tags to search for
  6. """
  7. if not t:
  8. raise ValueError("At least one tag must be specified")
  9. return TagValue(*t)
  10. def between(a, b, inclusive_min=True, inclusive_max=True):
  11. """
  12. Indicate that value is a numeric range
  13. """
  14. return RangeValue(a, b, inclusive_min=inclusive_min, inclusive_max=inclusive_max)
  15. def equal(n):
  16. """
  17. Match a numeric value
  18. """
  19. return between(n, n)
  20. def lt(n):
  21. """
  22. Match any value less than n
  23. """
  24. return between(None, n, inclusive_max=False)
  25. def le(n):
  26. """
  27. Match any value less or equal to n
  28. """
  29. return between(None, n, inclusive_max=True)
  30. def gt(n):
  31. """
  32. Match any value greater than n
  33. """
  34. return between(n, None, inclusive_min=False)
  35. def ge(n):
  36. """
  37. Match any value greater or equal to n
  38. """
  39. return between(n, None, inclusive_min=True)
  40. def geo(lat, lon, radius, unit="km"):
  41. """
  42. Indicate that value is a geo region
  43. """
  44. return GeoValue(lat, lon, radius, unit)
  45. class Value:
  46. @property
  47. def combinable(self):
  48. """
  49. Whether this type of value may be combined with other values
  50. for the same field. This makes the filter potentially more efficient
  51. """
  52. return False
  53. @staticmethod
  54. def make_value(v):
  55. """
  56. Convert an object to a value, if it is not a value already
  57. """
  58. if isinstance(v, Value):
  59. return v
  60. return ScalarValue(v)
  61. def to_string(self):
  62. raise NotImplementedError()
  63. def __str__(self):
  64. return self.to_string()
  65. class RangeValue(Value):
  66. combinable = False
  67. def __init__(self, a, b, inclusive_min=False, inclusive_max=False):
  68. if a is None:
  69. a = "-inf"
  70. if b is None:
  71. b = "inf"
  72. self.range = [str(a), str(b)]
  73. self.inclusive_min = inclusive_min
  74. self.inclusive_max = inclusive_max
  75. def to_string(self):
  76. return "[{1}{0[0]} {2}{0[1]}]".format(
  77. self.range,
  78. "(" if not self.inclusive_min else "",
  79. "(" if not self.inclusive_max else "",
  80. )
  81. class ScalarValue(Value):
  82. combinable = True
  83. def __init__(self, v):
  84. self.v = str(v)
  85. def to_string(self):
  86. return self.v
  87. class TagValue(Value):
  88. combinable = False
  89. def __init__(self, *tags):
  90. self.tags = tags
  91. def to_string(self):
  92. return "{" + " | ".join(str(t) for t in self.tags) + "}"
  93. class GeoValue(Value):
  94. def __init__(self, lon, lat, radius, unit="km"):
  95. self.lon = lon
  96. self.lat = lat
  97. self.radius = radius
  98. self.unit = unit
  99. def to_string(self):
  100. return f"[{self.lon} {self.lat} {self.radius} {self.unit}]"
  101. class Node:
  102. def __init__(self, *children, **kwparams):
  103. """
  104. Create a node
  105. ### Parameters
  106. - **children**: One or more sub-conditions. These can be additional
  107. `intersect`, `disjunct`, `union`, `optional`, or any other `Node`
  108. type.
  109. The semantics of multiple conditions are dependent on the type of
  110. query. For an `intersection` node, this amounts to a logical AND,
  111. for a `union` node, this amounts to a logical `OR`.
  112. - **kwparams**: key-value parameters. Each key is the name of a field,
  113. and the value should be a field value. This can be one of the
  114. following:
  115. - Simple string (for text field matches)
  116. - value returned by one of the helper functions
  117. - list of either a string or a value
  118. ### Examples
  119. Field `num` should be between 1 and 10
  120. ```
  121. intersect(num=between(1, 10)
  122. ```
  123. Name can either be `bob` or `john`
  124. ```
  125. union(name=("bob", "john"))
  126. ```
  127. Don't select countries in Israel, Japan, or US
  128. ```
  129. disjunct_union(country=("il", "jp", "us"))
  130. ```
  131. """
  132. self.params = []
  133. kvparams = {}
  134. for k, v in kwparams.items():
  135. curvals = kvparams.setdefault(k, [])
  136. if isinstance(v, (str, int, float)):
  137. curvals.append(Value.make_value(v))
  138. elif isinstance(v, Value):
  139. curvals.append(v)
  140. else:
  141. curvals.extend(Value.make_value(subv) for subv in v)
  142. self.params += [Node.to_node(p) for p in children]
  143. for k, v in kvparams.items():
  144. self.params.extend(self.join_fields(k, v))
  145. def join_fields(self, key, vals):
  146. if len(vals) == 1:
  147. return [BaseNode(f"@{key}:{vals[0].to_string()}")]
  148. if not vals[0].combinable:
  149. return [BaseNode(f"@{key}:{v.to_string()}") for v in vals]
  150. s = BaseNode(f"@{key}:({self.JOINSTR.join(v.to_string() for v in vals)})")
  151. return [s]
  152. @classmethod
  153. def to_node(cls, obj): # noqa
  154. if isinstance(obj, Node):
  155. return obj
  156. return BaseNode(obj)
  157. @property
  158. def JOINSTR(self):
  159. raise NotImplementedError()
  160. def to_string(self, with_parens=None):
  161. with_parens = self._should_use_paren(with_parens)
  162. pre, post = ("(", ")") if with_parens else ("", "")
  163. return f"{pre}{self.JOINSTR.join(n.to_string() for n in self.params)}{post}"
  164. def _should_use_paren(self, optval):
  165. if optval is not None:
  166. return optval
  167. return len(self.params) > 1
  168. def __str__(self):
  169. return self.to_string()
  170. class BaseNode(Node):
  171. def __init__(self, s):
  172. super().__init__()
  173. self.s = str(s)
  174. def to_string(self, with_parens=None):
  175. return self.s
  176. class IntersectNode(Node):
  177. """
  178. Create an intersection node. All children need to be satisfied in order for
  179. this node to evaluate as true
  180. """
  181. JOINSTR = " "
  182. class UnionNode(Node):
  183. """
  184. Create a union node. Any of the children need to be satisfied in order for
  185. this node to evaluate as true
  186. """
  187. JOINSTR = "|"
  188. class DisjunctNode(IntersectNode):
  189. """
  190. Create a disjunct node. In order for this node to be true, all of its
  191. children must evaluate to false
  192. """
  193. def to_string(self, with_parens=None):
  194. with_parens = self._should_use_paren(with_parens)
  195. ret = super().to_string(with_parens=False)
  196. if with_parens:
  197. return "(-" + ret + ")"
  198. else:
  199. return "-" + ret
  200. class DistjunctUnion(DisjunctNode):
  201. """
  202. This node is true if *all* of its children are false. This is equivalent to
  203. ```
  204. disjunct(union(...))
  205. ```
  206. """
  207. JOINSTR = "|"
  208. class OptionalNode(IntersectNode):
  209. """
  210. Create an optional node. If this nodes evaluates to true, then the document
  211. will be rated higher in score/rank.
  212. """
  213. def to_string(self, with_parens=None):
  214. with_parens = self._should_use_paren(with_parens)
  215. ret = super().to_string(with_parens=False)
  216. if with_parens:
  217. return "(~" + ret + ")"
  218. else:
  219. return "~" + ret
  220. def intersect(*args, **kwargs):
  221. return IntersectNode(*args, **kwargs)
  222. def union(*args, **kwargs):
  223. return UnionNode(*args, **kwargs)
  224. def disjunct(*args, **kwargs):
  225. return DisjunctNode(*args, **kwargs)
  226. def disjunct_union(*args, **kwargs):
  227. return DistjunctUnion(*args, **kwargs)
  228. def querystring(*args, **kwargs):
  229. return intersect(*args, **kwargs).to_string()