diff --git a/build.sbt b/build.sbt index c26ff418f..8a06dfc66 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,11 @@ lazy val scalaVersions = sparkMajorVer match { case "3" => Seq("2.12.21", "2.13.18") case _ => throw new IllegalArgumentException(s"Unsupported Spark version: $sparkVer.") } +lazy val antlr4ToolVersion = sys.props.getOrElse("spark.version", "3.5.8").substring(0, 1) match { + case "4" => "4.13.1" + case "3" => "4.9.3" + case v => throw new IllegalArgumentException(s"Unsupported Spark version major: $v") +} lazy val scalaVer = sys.props.getOrElse("scala.version", scalaVersions.head) lazy val defaultScalaTestVer = "3.2.19" lazy val jmhVersion = "1.37" @@ -75,14 +80,20 @@ lazy val commonSetting = Seq( "org.apache.spark" %% "spark-sql" % sparkVer % "provided" cross CrossVersion.for3Use2_13, "org.apache.spark" %% "spark-mllib" % sparkVer % "provided" cross CrossVersion.for3Use2_13, "org.slf4j" % "slf4j-api" % "2.0.17" % "provided", - "org.apache.datasketches" % "datasketches-java" % "6.2.0", // transitive dependency from Spark + "org.apache.datasketches" % "datasketches-java" % "6.2.0" % "provided", // transitive from Spark + "org.antlr" % "antlr4" % antlr4ToolVersion % "provided", // transitive from Spark "org.scalatest" %% "scalatest" % defaultScalaTestVer % Test, "com.github.zafarkhaja" % "java-semver" % "0.10.2" % Test), Compile / doc / scalacOptions ++= Seq( "-groups", "-implicits", "-skip-packages", - Seq("org.apache.spark").mkString(":")), + // org.apache.spark is skipped to avoid rendering transitive Spark types; the GQL query engine + // under org.graphframes.propertygraph.internal is entirely private[propertygraph] (and + // AstBuilder references generated ANTLR Java types), so it is skipped from rendered output too. + // The internal package is still type-checked so that the public PropertyGraphFrame, which calls + // into it, compiles in doc. + Seq("org.apache.spark", "org.graphframes.propertygraph.internal").mkString(":")), Test / doc / scalacOptions ++= Seq("-groups", "-implicits"), // Test settings @@ -158,6 +169,7 @@ lazy val graphx = (project in file("graphx")) lazy val core = (project in file("core")) .dependsOn(graphx) + .enablePlugins(GraphFramesAntlr4Plugin) .settings( commonSetting, name := "graphframes", @@ -165,6 +177,10 @@ lazy val core = (project in file("core")) // Export the JAR so that this can be excluded from shading in connect exportJars := true, + // Emit the generated GQL parser/lexer into the internal package so the + // (forthcoming) AstBuilder can import them. + antlr4GenPackage := Some("org.graphframes.propertygraph.internal"), + // Global settings Global / concurrentRestrictions := Seq(Tags.limitAll(1)), autoAPIMappings := true, diff --git a/core/src/main/antlr4/org/graphframes/propertygraph/internal/GqlLexer.g4 b/core/src/main/antlr4/org/graphframes/propertygraph/internal/GqlLexer.g4 new file mode 100644 index 000000000..b5b9a3f6c --- /dev/null +++ b/core/src/main/antlr4/org/graphframes/propertygraph/internal/GqlLexer.g4 @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +lexer grammar GqlLexer; + +// --------------------------------------------------------------------------- +// Keywords (case-insensitive). These MUST precede IDENTIFIER so that, on a +// tie, the keyword token wins over the identifier rule. +// --------------------------------------------------------------------------- +MATCH: M A T C H; +WHERE: W H E R E; +RETURN: R E T U R N; +AND: A N D; +OR: O R; +NOT: N O T; +AS: A S; +TRUE: T R U E; +FALSE: F A L S E; +NULL: N U L L; +IS: I S; +IN: I N; + +// --------------------------------------------------------------------------- +// Punctuation and operators. +// +// ANTLR4 uses maximal munch: the longest match always wins, and on ties the +// rule listed first wins. Multi-character tokens are therefore safe even when +// they share a prefix with a shorter one (e.g. '->' beats '-' regardless of +// ordering), but equal-length alternatives must be ordered with care. None of +// the tokens below collide on (length, prefix), so ordering within a group is +// not load-bearing; groups are kept before IDENTIFIER for clarity only. +// --------------------------------------------------------------------------- +ARROW_RIGHT: '->'; +ARROW_LEFT: '<-'; +LTE: '<='; +GTE: '>='; +NEQ: '<>'; +NEQ_BANG: '!='; +LT: '<'; +GT: '>'; +EQ: '='; +DASH: '-'; +PLUS: '+'; +STAR: '*'; +SLASH: '/'; +PERCENT: '%'; +DOT: '.'; +DOTDOT: '..'; +COMMA: ','; +COLON: ':'; +LPAREN: '('; +RPAREN: ')'; +LBRACK: '['; +RBRACK: ']'; +LBRACE: '{'; +RBRACE: '}'; + +// --------------------------------------------------------------------------- +// Literals +// --------------------------------------------------------------------------- + +// Single-quoted string with '' escape, per SQL/GQL convention. +STRING_LITERAL: '\'' ( ~'\'' | '\'\'' )* '\''; + +DECIMAL_LITERAL: DIGIT+ '.' DIGIT+; +INTEGER_LITERAL: DIGIT+; + +// --------------------------------------------------------------------------- +// Identifiers. Must follow all keyword rules so keywords win the tie. +// --------------------------------------------------------------------------- +IDENTIFIER: [a-zA-Z_] [a-zA-Z0-9_]*; + +// --------------------------------------------------------------------------- +// Whitespace and comments -> skipped. +// --------------------------------------------------------------------------- +WS: [ \t\r\n\u000C]+ -> skip; +LINE_COMMENT: '//' ~[\r\n]* -> skip; +BLOCK_COMMENT: '/*' .*? '*/' -> skip; + +// --------------------------------------------------------------------------- +// Fragments +// --------------------------------------------------------------------------- +fragment DIGIT: [0-9]; + +// Letter fragments used to build case-insensitive keywords. +fragment A: ('a' | 'A'); +fragment B: ('b' | 'B'); +fragment C: ('c' | 'C'); +fragment D: ('d' | 'D'); +fragment E: ('e' | 'E'); +fragment F: ('f' | 'F'); +fragment G: ('g' | 'G'); +fragment H: ('h' | 'H'); +fragment I: ('i' | 'I'); +fragment J: ('j' | 'J'); +fragment K: ('k' | 'K'); +fragment L: ('l' | 'L'); +fragment M: ('m' | 'M'); +fragment N: ('n' | 'N'); +fragment O: ('o' | 'O'); +fragment P: ('p' | 'P'); +fragment Q: ('q' | 'Q'); +fragment R: ('r' | 'R'); +fragment S: ('s' | 'S'); +fragment T: ('t' | 'T'); +fragment U: ('u' | 'U'); +fragment V: ('v' | 'V'); +fragment W: ('w' | 'W'); +fragment X: ('x' | 'X'); +fragment Y: ('y' | 'Y'); +fragment Z: ('z' | 'Z'); diff --git a/core/src/main/antlr4/org/graphframes/propertygraph/internal/GqlParser.g4 b/core/src/main/antlr4/org/graphframes/propertygraph/internal/GqlParser.g4 new file mode 100644 index 000000000..32e28261d --- /dev/null +++ b/core/src/main/antlr4/org/graphframes/propertygraph/internal/GqlParser.g4 @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +parser grammar GqlParser; + +options { + tokenVocab = GqlLexer; +} + +// --------------------------------------------------------------------------- +// Top-level statement. +// +// RETURN is optional in the grammar so the engine can default to returning +// matched IDs when the user omits it (forward-compatible; trivially tightened +// later by making RETURN mandatory). +// --------------------------------------------------------------------------- +gqlStatement + : MATCH matchPattern (WHERE whereClause)? (RETURN returnClause)? EOF + ; + +// A match pattern is a chain of alternating nodes and directed edges, +// e.g. (a:Person)-[:KNOWS]->(b:Person)-[:WORKS_AT]->(c:Company). +matchPattern + : nodePattern (edgePattern nodePattern)* + ; + +// Node pattern: typed (a:Person), untyped (x), or anonymous (). +nodePattern + : LPAREN (variable=IDENTIFIER)? (COLON label=IDENTIFIER)? RPAREN + ; + +// Edge pattern. +// +// Three forms: +// -[e:KNOWS]-> (left-to-right) +// <-[e:KNOWS]- (right-to-left) +// -[:KNOWS]- (undirected) +// The edge body [variable? :label?] is shared via edgeBody. +edgePattern + : DASH edgeBody ARROW_RIGHT // a -[e]-> b + | ARROW_LEFT edgeBody DASH // a <-[e]- b + | DASH edgeBody DASH // a -[e]- b + ; + +edgeBody + : LBRACK (variable=IDENTIFIER)? (COLON label=IDENTIFIER)? quantifier? RBRACK + ; + +// Variable-length pattern: [KNOWS*1..3] or [KNOWS*3] +quantifier + : STAR lo=INTEGER_LITERAL DOTDOT hi=INTEGER_LITERAL // *1..3 (bounded range) + | STAR exact=INTEGER_LITERAL // *3 (exactly 3 hops) + ; + +// --------------------------------------------------------------------------- +// WHERE clause: a single boolean expression. +// --------------------------------------------------------------------------- +whereClause + : expression + ; + +// --------------------------------------------------------------------------- +// RETURN clause: either SELECT * or a comma-separated list of items. +// --------------------------------------------------------------------------- +returnClause + : STAR + | returnItem (COMMA returnItem)* + ; + +returnItem + : expression (AS alias=IDENTIFIER)? + ; + +// --------------------------------------------------------------------------- +// Expression grammar. +// +// Precedence (lowest -> highest): OR < AND < NOT < comparison < additive < +// multiplicative < primary. Standard recursive-descent shape; ANTLR4 resolves +// left-recursive alternatives correctly. +// --------------------------------------------------------------------------- +expression + : orExpr + ; + +orExpr + : andExpr (OR andExpr)* + ; + +andExpr + : notExpr (AND notExpr)* + ; + +notExpr + : NOT notExpr + | comparison + ; + +comparison + : additive (compOp additive)? + ; + +additive + : multiplicative ((PLUS | DASH) multiplicative)* + ; + +multiplicative + : primary ((STAR | SLASH | PERCENT) primary)* + ; + +primary + : LPAREN expression RPAREN + | literal + | functionCall + | propertyAccess + | variable=IDENTIFIER + ; + +functionCall + : name=IDENTIFIER LPAREN ( expression ( COMMA expression )* )? RPAREN + ; + +propertyAccess + : variable=IDENTIFIER DOT property=IDENTIFIER + ; + +compOp + : EQ + | NEQ + | NEQ_BANG + | LT + | LTE + | GT + | GTE + ; + +literal + : INTEGER_LITERAL + | DECIMAL_LITERAL + | STRING_LITERAL + | TRUE + | FALSE + | NULL + ; diff --git a/core/src/main/scala/org/graphframes/propertygraph/PropertyGraphFrame.scala b/core/src/main/scala/org/graphframes/propertygraph/PropertyGraphFrame.scala index ac1ff9c1f..254f22476 100644 --- a/core/src/main/scala/org/graphframes/propertygraph/PropertyGraphFrame.scala +++ b/core/src/main/scala/org/graphframes/propertygraph/PropertyGraphFrame.scala @@ -5,6 +5,13 @@ import org.apache.spark.sql.DataFrame import org.apache.spark.sql.functions.col import org.apache.spark.sql.functions.lit import org.graphframes.GraphFrame +import org.graphframes.propertygraph.internal.AstBuilder +import org.graphframes.propertygraph.internal.GqlExplain +import org.graphframes.propertygraph.internal.JoinOptimizer +import org.graphframes.propertygraph.internal.QueryExecutor +import org.graphframes.propertygraph.internal.ResolvedQuery +import org.graphframes.propertygraph.internal.Resolver +import org.graphframes.propertygraph.internal.SchemaGraphSnapshot import org.graphframes.propertygraph.property.EdgePropertyGroup import org.graphframes.propertygraph.property.VertexPropertyGroup @@ -34,10 +41,181 @@ case class PropertyGraphFrame( vertexPropertyGroups: Seq[VertexPropertyGroup], edgesPropertyGroups: Seq[EdgePropertyGroup]) { import PropertyGraphFrame._ - lazy private val vertexGroups: Map[String, VertexPropertyGroup] = - vertexPropertyGroups.map(pg => pg.name -> pg).toMap - lazy private val edgeGroups: Map[String, EdgePropertyGroup] = - edgesPropertyGroups.map(pg => pg.name -> pg).toMap + + // Keys are lowercased so that lookups in toGraphFrame and projectionBy are case-insensitive. + // It is an overall policy across all the LPG functionality. + lazy private[propertygraph] val vertexGroups: Map[String, VertexPropertyGroup] = + vertexPropertyGroups.map(pg => pg.name.toLowerCase -> pg).toMap + lazy private[propertygraph] val edgeGroups: Map[String, EdgePropertyGroup] = + edgesPropertyGroups.map(pg => pg.name.toLowerCase -> pg).toMap + + lazy private[propertygraph] val schemaGraphSnapshot: SchemaGraphSnapshot = + SchemaGraphSnapshot.fromPropertyGraphFrame(this) + + /** + * Returns a human-readable description of the property graph schema. + * + * The output lists all vertex property groups and edge property groups with their + * source/destination connections, sorted alphabetically for determinism. + * + * @return + * a multi-line string describing the graph schema + */ + def schemaString: String = SchemaGraphSnapshot.toString(schemaGraphSnapshot) + + /** + * Returns the property graph schema in DOT (Graphviz) format. + * + * The output is a valid `digraph` that can be rendered by Graphviz tools. Vertex property + * groups appear as nodes and edge property groups appear as directed edges labeled with the + * group name. + * + * @return + * a DOT-format string representing the graph schema + */ + def schemaStringDOT: String = SchemaGraphSnapshot.toDOT(schemaGraphSnapshot) + + /** + * Executes a GQL `MATCH` query against this property graph and returns the matched paths as a + * Spark DataFrame with a fixed output schema: + * - `start_id`, `start_property_group`, `end_id`, `end_property_group`, + * `edge_property_group`, and a + * `path: array>` column for + * intermediate hops. + * + * This is a convenience overload equivalent to `query(gql, QueryOptions())`. + * + * @param gql + * a GQL `MATCH` statement in the supported subset. + * @return + * a DataFrame over the fixed output schema. + */ + def query(gql: String): DataFrame = query(gql, QueryOptions()) + + /** + * Executes a GQL `MATCH` query against this property graph and returns the matched paths as a + * Spark DataFrame with a fixed output schema: + * - `start_id`, `start_property_group`, `end_id`, `end_property_group`, + * `edge_property_group`, and a + * `path: array>` column for + * intermediate hops. + * + * The query is compiled through: ANTLR parse -> AST -> schema resolution -> join planning -> + * DataFrame execution (per-path `UNION ALL`). Disconnected patterns (no schema path matches) + * return an empty DataFrame without touching data. Bad syntax throws + * [[org.graphframes.InvalidParseException]]; unknown labels throw + * [[org.graphframes.InvalidPropertyGroupException]]. + * + * @param gql + * a GQL `MATCH` statement in the supported subset. + * @param options + * query options + * @return + * a DataFrame over the fixed output schema. + */ + def query(gql: String, options: QueryOptions): DataFrame = { + val resolved = + resolve(gql, options, enforceMaxSchemaPathLength = true, enforceMaxPathsCount = true) + if (resolved.paths.isEmpty) { + return QueryExecutor.execute(this, Seq.empty) + } + + // Cost-based optimization and statistics will follow + val _ = options.enableStatistics // placeholder + val plans = JoinOptimizer.plan(resolved, stats = None) + QueryExecutor.execute(this, plans) + } + + /** + * Renders the logical (resolved) plan of `gql` without executing it. + * + * This is a convenience overload equivalent to `fexplain(gql, ExplainMode.Logical)`. To see the + * per-path join plans (order + statistics basis). + * + * @param gql + * a GQL `MATCH` statement in the supported subset. + * @return + * a string describing the resolved (logical) plan. + */ + def explain(gql: String): String = explain(gql, ExplainMode.Logical) + + /** + * Renders a plan of `gql` without executing it, using default query options. + * + * This is a convenience overload equivalent to `explain(gql, mode, QueryOptions())`. Pass + * [[ExplainMode.Physical]] to see the per-path join plans (order + statistics basis); + * [[ExplainMode.Logical]] shows the resolved (logical) plan. + * + * @param gql + * a GQL `MATCH` statement in the supported subset. + * @param mode + * the explain mode: [[ExplainMode.Logical]] for the resolved plan or [[ExplainMode.Physical]] + * for the per-path join plans. + * @return + * a string describing the requested plan. + */ + def explain(gql: String, mode: ExplainMode): String = explain(gql, mode, QueryOptions()) + + /** + * Renders a plan of `gql` without executing it. + * + * Pass [[ExplainMode.Physical]] to see the per-path join plans (order + statistics basis); + * [[ExplainMode.Logical]] shows the resolved (logical) plan. + * + * @param gql + * a GQL `MATCH` statement in the supported subset. + * @param mode + * the explain mode: [[ExplainMode.Logical]] for the resolved plan or [[ExplainMode.Physical]] + * for the per-path join plans. + * @param options + * query options. + * @return + * a string describing the requested plan. + */ + def explain(gql: String, mode: ExplainMode, options: QueryOptions): String = { + // users should be able to see paths that exceed maxSchemaPathLength + // even if it is not allowed for real queries. + val resolved = + resolve(gql, options, enforceMaxSchemaPathLength = false, enforceMaxPathsCount = false) + mode match { + case ExplainMode.Logical => GqlExplain.logical(resolved) + case ExplainMode.Physical => + val plans = JoinOptimizer.plan(resolved, stats = None) + GqlExplain.physical(plans) + } + } + + /** + * Shared parse + resolve step for [[query]] and [[explain]]. Applies `maxSchemaPathLength` as a + * guard against pathological enumeration depth when [[enforceMaxSchemaPathLength]] is true (the + * [[query]] path). The [[explain]] path sets it to false so users can inspect the plan that + * exceeds the cap and understand why [[query]] rejects it. + */ + private def resolve( + gql: String, + options: QueryOptions, + enforceMaxSchemaPathLength: Boolean, + enforceMaxPathsCount: Boolean): ResolvedQuery = { + require( + options.maxSchemaPathLength > 0, + s"maxSchemaPathLength must be positive, got ${options.maxSchemaPathLength}") + val ast = AstBuilder.parse(gql) + val resolved = Resolver.resolve(ast, schemaGraphSnapshot, options) + if (enforceMaxSchemaPathLength) { + resolved.paths.foreach { path => + require( + path.length <= options.maxSchemaPathLength, + s"Schema path length ${path.length} exceeds maxSchemaPathLength=${options.maxSchemaPathLength}: " + + s"$path; try to rewrite the query and reduce a potential depth. Use `explain` to see the plan.") + } + } + if (enforceMaxPathsCount) { + require( + resolved.paths.size <= options.maxEnumeratedPaths, + s"An amount of paths in the resolved query exceeds ${options.maxEnumeratedPaths}: " + "either use `explain` and modify the pattern or increase the value in `QueryOptions`") + } + resolved + } /** * Converts a heterogeneous property graph into a unified GraphFrame representation. @@ -73,16 +251,18 @@ case class PropertyGraphFrame( edgeGroupFilters: Map[String, Column], vertexGroupFilters: Map[String, Column]): GraphFrame = { vertexPropertyGroups.foreach(name => - require(vertexGroups.contains(name), s"Vertex property group $name does not exist")) + require( + vertexGroups.contains(name.toLowerCase), + s"Vertex property group $name does not exist")) edgePropertyGroups.foreach(name => - require(edgeGroups.contains(name), s"Edge property group $name does not exist")) + require(edgeGroups.contains(name.toLowerCase), s"Edge property group $name does not exist")) val vertices = vertexPropertyGroups - .map(name => vertexGroups(name).getData(vertexGroupFilters(name))) + .map(name => vertexGroups(name.toLowerCase).getData(vertexGroupFilters(name))) .reduce(_ union _) val edges = edgePropertyGroups - .map(name => edgeGroups(name).getData(edgeGroupFilters(name))) + .map(name => edgeGroups(name.toLowerCase).getData(edgeGroupFilters(name))) .reduce(_ union _) GraphFrame(vertices, edges) @@ -111,15 +291,18 @@ case class PropertyGraphFrame( rightBiGraphPart: String, edgeGroup: String, newEdgeWeight: Option[(Column, Column) => Column] = None): PropertyGraphFrame = { + // Hoisted before the require checks so the lowercased lookup is performed only once. + val oldGroup = edgeGroups(edgeGroup.toLowerCase) require( - edgeGroups(edgeGroup).srcPropertyGroup.name == leftBiGraphPart, - s"Edge Property Group should have $leftBiGraphPart source group but has ${edgeGroups(edgeGroup).srcPropertyGroup.name}") + oldGroup.srcPropertyGroup.name.equalsIgnoreCase(leftBiGraphPart), + s"Edge Property Group should have $leftBiGraphPart source group but has ${oldGroup.srcPropertyGroup.name}") require( - edgeGroups(edgeGroup).dstPropertyGroup.name == rightBiGraphPart, - s"Edge Property Group should have $rightBiGraphPart destination group but has ${edgeGroups(edgeGroup).dstPropertyGroup.name}") - val keptVPropertyGroups = vertexPropertyGroups.filterNot(g => g.name == rightBiGraphPart) - val keptEPropertyGroups = edgesPropertyGroups.filterNot(g => g.name == edgeGroup) - val oldGroup = edgeGroups(edgeGroup) + oldGroup.dstPropertyGroup.name.equalsIgnoreCase(rightBiGraphPart), + s"Edge Property Group should have $rightBiGraphPart destination group but has ${oldGroup.dstPropertyGroup.name}") + val keptVPropertyGroups = + vertexPropertyGroups.filterNot(g => g.name.equalsIgnoreCase(rightBiGraphPart)) + val keptEPropertyGroups = + edgesPropertyGroups.filterNot(g => g.name.equalsIgnoreCase(edgeGroup)) val oldEdgesData = oldGroup.data // Create new edges by joining vertices through their common neighbors @@ -141,8 +324,8 @@ case class PropertyGraphFrame( val newEdgeGroup = EdgePropertyGroup( name = s"projected_$edgeGroup", data = projectedEdges, - srcPropertyGroup = vertexGroups(leftBiGraphPart), - dstPropertyGroup = vertexGroups(leftBiGraphPart), + srcPropertyGroup = vertexGroups(leftBiGraphPart.toLowerCase), + dstPropertyGroup = vertexGroups(leftBiGraphPart.toLowerCase), isDirected = false, srcColumnName = GraphFrame.SRC, dstColumnName = GraphFrame.DST, diff --git a/core/src/main/scala/org/graphframes/propertygraph/QueryOptions.scala b/core/src/main/scala/org/graphframes/propertygraph/QueryOptions.scala new file mode 100644 index 000000000..8a8707aa5 --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/QueryOptions.scala @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph + +/** + * Options for query resolution and optimization. + * + * @param enableStatistics + * whether the optimizer may consume statistics for join ordering (default `true`). + * @param maxSchemaPathLength + * cap on schema-path enumeration depth, to bound the fan-out of untyped/ambiguous patterns + * (default `10`). + * @param maxVarLength + * maximum length of a variable-length pattern (`[e*1..N]`), controlling how many hops a + * repeating edge pattern may expand to (default `5`). + * @param maxEnumeratedPaths + * maximal number of paths in the schema-graph that will be processed; each path results in one + * Spark SQL execution plan and all of them are unioned at the end (default `32`). + */ +final case class QueryOptions( + enableStatistics: Boolean = true, + maxSchemaPathLength: Int = 10, + maxVarLength: Int = 5, + maxEnumeratedPaths: Int = 32) + +object QueryOptions { + + /** + * Creates a new [[QueryOptions]] instance with all fields set to their default values. + * + * This is a convenience factory method intended primarily for Java and Py4J callers, who cannot + * use Scala's default argument syntax directly. Scala users should prefer the default + * constructor, e.g. `QueryOptions()`. + * + * @return + * a fresh [[QueryOptions]] with + * - `enableStatistics` = `true` + * - `maxSchemaPathLength` = `10` + * - `maxVarLength` = `5` + * - `maxEnumeratedPaths` = `32` + */ + def withDefualts: QueryOptions = QueryOptions() + + /** + * Creates a new [[QueryOptions]] instance with the specified + * [[QueryOptions.maxSchemaPathLength maxSchemaPathLength]] value, leaving all other fields at + * their defaults. + * + * This is a convenience factory method intended primarily for Java and Py4J callers who cannot + * use Scala named-argument syntax directly. Scala users should prefer + * `QueryOptions(maxSchemaPathLength = n)`. + * + * @param maxSchemaPathLength + * the maximum schema-path enumeration depth to use; see [[QueryOptions.maxSchemaPathLength]] + * for the effect of this setting. Must be non-negative. + * @return + * a new [[QueryOptions]] with + * - `enableStatistics` = `true` + * - `maxSchemaPathLength` = the supplied value + * - `maxVarLength` = `5` + * - `maxEnumeratedPaths` = `32` + */ + def withMaxSchemaPathLength(maxSchemaPathLength: Int): QueryOptions = + QueryOptions(maxSchemaPathLength = maxSchemaPathLength) +} + +/** Selects which plan to render via explain. */ +sealed trait ExplainMode + +object ExplainMode { + case object Logical extends ExplainMode + case object Physical extends ExplainMode +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/AstBuilder.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/AstBuilder.scala new file mode 100644 index 000000000..68e7328b2 --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/AstBuilder.scala @@ -0,0 +1,297 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +import org.antlr.v4.runtime.BailErrorStrategy +import org.antlr.v4.runtime.CharStreams +import org.antlr.v4.runtime.CommonTokenStream +import org.antlr.v4.runtime.RecognitionException +import org.antlr.v4.runtime.misc.ParseCancellationException +import org.antlr.v4.runtime.tree.TerminalNode +import org.graphframes.GraphFramesUnreachableException +import org.graphframes.InvalidParseException + +import scala.jdk.CollectionConverters.* + +/** + * Lowers an ANTLR parse tree (produced by the generated `GqlParser`) into the hand-written + * `GqlAst`. This is the only place in the engine that touches generated `*Context` types. No + * ANTLR `*Context`/`GqlParser*` type escapes this file. + */ +private[propertygraph] object AstBuilder { + + /** Parse a GQL string into a `MatchStatement`, or throw `InvalidParseException`. */ + def parse(gql: String): MatchStatement = { + val parsed = + try { + val chars = CharStreams.fromString(gql) + val lexer = new GqlLexer(chars) + lexer.removeErrorListeners() + val tokens = new CommonTokenStream(lexer) + val parser = new GqlParser(tokens) + parser.removeErrorListeners() + parser.setErrorHandler(new BailErrorStrategy()) + parser.gqlStatement() + } catch { + case e: ParseCancellationException => + val cause = Option(e.getCause).map(_.getMessage).getOrElse(e.getMessage) + throw new InvalidParseException(s"Failed to parse GQL query: $cause") + case e: RecognitionException => + throw new InvalidParseException(s"Failed to parse GQL query: ${e.getMessage}") + } + // visit returns AnyRef (visitGqlStatement returns MatchStatement). + new AstBuilder().visit(parsed).asInstanceOf[MatchStatement] + } +} + +// Visitor type is AnyRef because ANTLR's generic T must be a reference type, and different rules +// produce different node kinds (MatchStatement, PatternElement, Expression, ...). +private[propertygraph] final class AstBuilder extends GqlParserBaseVisitor[AnyRef] { + + override def visitGqlStatement(ctx: GqlParser.GqlStatementContext): MatchStatement = { + val pattern = visitMatchPattern(ctx.matchPattern()) + val where = Option(ctx.whereClause()).map(c => visitExpression(c.expression())) + val returnClause = Option(ctx.returnClause()).map(visitReturnClause) + MatchStatement(pattern, where, returnClause) + } + + // matchPattern: nodePattern (edgePattern nodePattern)* + // Reinterleave into the user-written order: N0, E0, N1, E1, N2, ... + override def visitMatchPattern(ctx: GqlParser.MatchPatternContext): GraphPattern = { + val nodes = ctx.nodePattern().asScala.map(visitNodePattern) + val edges = ctx.edgePattern().asScala.map(visitEdgePattern) + val elements = scala.collection.mutable.ListBuffer.empty[PatternElement] + elements += nodes.head + edges.zip(nodes.tail).foreach { case (e, n) => + elements += e + elements += n + } + GraphPattern(elements.toSeq) + } + + // nodePattern: LPAREN (variable=IDENTIFIER)? (COLON label=IDENTIFIER)? RPAREN + // The generated context exposes IDENTIFIERs as a flat list; use COLON presence to disambiguate. + override def visitNodePattern(ctx: GqlParser.NodePatternContext): NodePattern = { + val (variable, label) = + readVariableLabel(ctx.IDENTIFIER().asScala.map(_.getText).toSeq, ctx.COLON()) + NodePattern(variable, label) + } + + // edgePatterns: + // - DASH edgeBody ARROW_RIGHT + // - ARROW_LEFT edgeBody DASH + // - DASH edgeBody DASH + override def visitEdgePattern(ctx: GqlParser.EdgePatternContext): EdgePattern = { + val direction = + if (ctx.ARROW_RIGHT() == null && ctx.ARROW_LEFT() == null) Undirected + else if (ctx.ARROW_RIGHT() != null) LeftToRight + else RightToLeft + val (variable, label) = + readVariableLabel( + ctx.edgeBody().IDENTIFIER().asScala.map(_.getText).toSeq, + ctx.edgeBody().COLON()) + + // *N or *A..B + val hopsRange = Option(ctx.edgeBody().quantifier()).map(qCtx => { + // Variable-length patterns are not compatible with variables + if (variable.isDefined) { + throw new InvalidParseException( + "using variables for variable length patterns is not supported") + } + if (qCtx.exact != null) { + (qCtx.exact.getText.toInt, qCtx.exact.getText.toInt) + } else { + (qCtx.lo.getText.toInt, qCtx.hi.getText.toInt) + } + }) + EdgePattern(variable, label, direction, hopsRange) + } + + override def visitWhereClause(ctx: GqlParser.WhereClauseContext): Expression = + visitExpression(ctx.expression()) + + override def visitReturnClause(ctx: GqlParser.ReturnClauseContext): ReturnClause = { + if (ctx.STAR() != null) { + ReturnStar + } else { + ReturnItems(ctx.returnItem().asScala.map(visitReturnItem).toSeq) + } + } + + override def visitReturnItem(ctx: GqlParser.ReturnItemContext): ReturnItem = { + val expr = visitExpression(ctx.expression()) + val alias = Option(ctx.IDENTIFIER()).map(_.getText) + ReturnItem(expr, alias) + } + + // ------------------------------------------------------------------------- + // Expression tier. Each level folds left-associative chains; NOT is prefix + // and stacks (NOT NOT a > b). + // ------------------------------------------------------------------------- + + override def visitExpression(ctx: GqlParser.ExpressionContext): Expression = + visitOrExpr(ctx.orExpr()) + + override def visitOrExpr(ctx: GqlParser.OrExprContext): Expression = { + val parts = ctx.andExpr().asScala.map(visitAndExpr) + parts.reduceLeftOption(Or.apply).getOrElse(Literal(true)) + } + + override def visitAndExpr(ctx: GqlParser.AndExprContext): Expression = { + val parts = ctx.notExpr().asScala.map(visitNotExpr) + parts.reduceLeftOption(And.apply).getOrElse(Literal(true)) + } + + override def visitNotExpr(ctx: GqlParser.NotExprContext): Expression = { + if (ctx.NOT() != null) { + Not(visitNotExpr(ctx.notExpr())) + } else { + visitComparison(ctx.comparison()) + } + } + + override def visitComparison(ctx: GqlParser.ComparisonContext): Expression = { + val left = visitAdditive(ctx.additive(0)) + if (ctx.compOp() != null) { + val op = visitCompOp(ctx.compOp()) + val right = visitAdditive(ctx.additive(1)) + Comparison(left, op, right) + } else { + left + } + } + + // additive: multiplicative ((PLUS | DASH) multiplicative)* (left-associative) + // ANTLR exposes PLUS and DASH as two separate token lists, which loses their interleaving. We + // instead walk the context's children in source order so operators stay aligned with their + // multiplicatives. + override def visitAdditive(ctx: GqlParser.AdditiveContext): Expression = { + val ops: Seq[AddOp] = ctx.children.asScala.collect { + case t if t.getText == "+" => Plus: AddOp + case t if t.getText == "-" => Minus: AddOp + }.toSeq + val parts = ctx.multiplicative().asScala.map(visitMultiplicative).toSeq + parts.tail.zip(ops).foldLeft(parts.head: Expression) { case (acc, (rhs, op)) => + Arithmetic(acc, op, rhs) + } + } + + // multiplicative: primary ((STAR | SLASH | PERCENT) primary)* (left-associative) + // Same source-order child walk as visitAdditive; STAR/SLASH/PERCENT are distinct tokens so we + // collect them by text to keep operators aligned with their primaries. + override def visitMultiplicative(ctx: GqlParser.MultiplicativeContext): Expression = { + val ops: Seq[MulOp] = ctx.children.asScala.collect { + case t if t.getText == "*" => Mult: MulOp + case t if t.getText == "/" => Div: MulOp + case t if t.getText == "%" => Mod: MulOp + }.toSeq + val primaries = ctx.primary().asScala.map(visitPrimary).toSeq + primaries.tail.zip(ops).foldLeft(primaries.head: Expression) { case (acc, (rhs, op)) => + Arithmetic(acc, op, rhs) + } + } + + override def visitPrimary(ctx: GqlParser.PrimaryContext): Expression = { + if (ctx.functionCall() != null) { + visitFunctionCall(ctx.functionCall()) + } else if (ctx.LPAREN() != null) { + visitExpression(ctx.expression()) + } else if (ctx.literal() != null) { + visitLiteral(ctx.literal()) + } else if (ctx.propertyAccess() != null) { + visitPropertyAccess(ctx.propertyAccess()) + } else { + Variable(ctx.IDENTIFIER().getText) + } + } + + override def visitFunctionCall(ctx: GqlParser.FunctionCallContext): FunctionCall = { + val name = ctx.name.getText.toLowerCase() + val args = ctx.expression().asScala.map(visitExpression).toSeq + FunctionCall(name, args) + } + + override def visitPropertyAccess(ctx: GqlParser.PropertyAccessContext): Expression = { + val ids = ctx.IDENTIFIER().asScala.map(_.getText) + // propertyAccess: variable=IDENTIFIER DOT property=IDENTIFIER -> exactly two identifiers. + PropertyAccess(ids(0), ids(1)) + } + + override def visitCompOp(ctx: GqlParser.CompOpContext): CompOp = { + if (ctx.EQ() != null) Eq + else if (ctx.NEQ() != null || ctx.NEQ_BANG() != null) Neq + else if (ctx.LT() != null) Lt + else if (ctx.LTE() != null) Lte + else if (ctx.GT() != null) Gt + else if (ctx.GTE() != null) Gte + else { + throw new GraphFramesUnreachableException() + } + } + + override def visitLiteral(ctx: GqlParser.LiteralContext): Literal = { + val value: Any = + if (ctx.INTEGER_LITERAL() != null) { + ctx.INTEGER_LITERAL().getText.toLong + } else if (ctx.DECIMAL_LITERAL() != null) { + ctx.DECIMAL_LITERAL().getText.toDouble + } else if (ctx.STRING_LITERAL() != null) { + unquoteString(ctx.STRING_LITERAL().getText) + } else if (ctx.TRUE() != null) { + java.lang.Boolean.TRUE + } else if (ctx.FALSE() != null) { + java.lang.Boolean.FALSE + } else { + null // NULL + } + Literal(value) + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** + * Recover `(variable, label)` from a node/edge body's identifier list. + * + * Grammar shapes (both `(variable)? (COLON label)?`): + * - 0 identifiers, no colon -> `(None, None)` + * - 1 identifier, no colon -> `(Some(var), None)` + * - 1 identifier, colon -> `(None, Some(label))` (label-only, e.g. `(:Person)`) + * - 2 identifiers, colon -> `(Some(var), Some(label))` + */ + private def readVariableLabel( + ids: Seq[String], + colon: TerminalNode): (Option[String], Option[String]) = { + if (colon == null) { + (ids.headOption, None) + } else { + ids.size match { + case 1 => (None, Some(ids.head)) + case 2 => (Some(ids.head), Some(ids(1))) + case _ => (None, None) // unreachable given the grammar + } + } + } + + /** Strip the surrounding single quotes and undo the `''` escape. */ + private def unquoteString(raw: String): String = { + val withoutQuotes = raw.substring(1, raw.length - 1) + withoutQuotes.replace("''", "'") + } +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/FunctionRegistry.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/FunctionRegistry.scala new file mode 100644 index 000000000..e8c895003 --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/FunctionRegistry.scala @@ -0,0 +1,295 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +import org.apache.spark.sql.Column +import org.apache.spark.sql.functions +import org.apache.spark.sql.functions.* + +/** + * Whitelist-dispatched lowering of scalar function calls (`FunctionCall` AST nodes) to Spark SQL + * built-in functions. + * + * Design: the registry is a strict 1:1 map from a (case-insensitive) function name to a single + * `org.apache.spark.sql.functions` builtin. The whitelist ''is'' the scope boundary: + * + * - Unknown names fail fast with `UnsupportedOperationException` naming the supported set -- + * the same fail-fast philosophy as the resolver's unknown-label error. + * - Wrong arity fails fast with a clear message (`expects N argument(s), got M`). + * - The function name in the AST is lowercased at parse time; this layer normalizes once more + * for defense-in-depth. + * + * Two argument kinds are supported: + * - ordinary column arguments (`cols(i)`) lowered from the AST; + * - ''literal-typed'' arguments (`litStr`/`litInt`), for Spark builtins whose signature demands + * a `String`/`Int` literal rather than a `Column` (e.g. `get_json_object(col, path: String)`, + * `round(col, scale: Int)`). A non-literal passed where a literal is required fails fast with + * a clear message. + * + * UDFs, custom functions, etc. are not supported. While it is possible to extend the support, I + * see no reason in it. At least until there is no explicit user-request. + */ +private[propertygraph] object FunctionRegistry { + + /** + * Lower `rawName(astArgs)` to a Spark [[org.apache.spark.sql.Column]], validating name + arity. + * + * `astArgs` are the raw AST arguments (needed to extract literal-typed args); `cols` are the + * same arguments already lowered to Spark columns (for ordinary column-arg builtins). + */ + def lower(rawName: String, astArgs: Seq[Expression], cols: Seq[Column]): Column = { + val name = rawName.toLowerCase + def arity(n: Int): Unit = + if (cols.length != n) { + throw new UnsupportedOperationException( + s"Function '$rawName' expects $n argument(s), got ${cols.length}") + } + def arityBetween(lo: Int, hi: Int): Unit = + if (cols.length < lo || cols.length > hi) { + throw new UnsupportedOperationException( + s"Function '$rawName' expects between $lo and $hi argument(s), got ${cols.length}") + } + def arityAtLeast(n: Int): Unit = + if (cols.length < n) { + throw new UnsupportedOperationException( + s"Function '$rawName' expects at least $n argument(s), got ${cols.length}") + } + + name match { + // *************** DATETIME BLOCK ********************************// + case "year" => arity(1); year(cols(0)) + case "month" => arity(1); month(cols(0)) + case "day" | "dayofmonth" => arity(1); dayofmonth(cols(0)) + case "hour" => arity(1); hour(cols(0)) + case "minute" => arity(1); minute(cols(0)) + case "second" => arity(1); second(cols(0)) + case "quarter" => arity(1); quarter(cols(0)) + case "dayofweek" => arity(1); dayofweek(cols(0)) + case "dayofyear" => arity(1); dayofyear(cols(0)) + case "weekofyear" => arity(1); weekofyear(cols(0)) + case "date" | "to_date" => arity(1); to_date(cols(0)) // string/ts -> date + case "to_timestamp" => arity(1); to_timestamp(cols(0)) + case "datediff" => arity(2); datediff(cols(0), cols(1)) // days, arg0 - arg1 + case "months_between" => arity(2); months_between(cols(0), cols(1)) + case "date_add" => arity(2); date_add(cols(0), cols(1)) + case "date_sub" => arity(2); date_sub(cols(0), cols(1)) + case "add_months" => arity(2); add_months(cols(0), cols(1)) + case "current_date" => arity(0); current_date() + case "current_timestamp" => arity(0); current_timestamp() + // **************************************************************// + + // *************** STRING BLOCK **********************************// + case "lower" => arity(1); functions.lower(cols(0)) + case "upper" => arity(1); upper(cols(0)) + case "trim" => arity(1); trim(cols(0)) + case "ltrim" => arity(1); ltrim(cols(0)) + case "rtrim" => arity(1); rtrim(cols(0)) + case "length" => arity(1); length(cols(0)) + case "substr" | "substring" => + arity(3); cols(0).substr(cols(1), cols(2)) // Column.substr(Column, Column) + case "concat" => arityAtLeast(1); concat(cols: _*) + case "regexp_replace" => arity(3); regexp_replace(cols(0), cols(1), cols(2)) + case "regexp_extract" => + arity(3); regexp_extract(cols(0), litStr(astArgs(1)), litInt(astArgs(2))) + case "rlike" | "regexp_like" => arity(2); cols(0).rlike(litStr(astArgs(1))) + case "contains" => arity(2); cols(0).contains(cols(1)) + case "startswith" => arity(2); cols(0).startsWith(cols(1)) + case "endswith" => arity(2); cols(0).endsWith(cols(1)) + case "instr" => arity(2); instr(cols(0), litStr(astArgs(1))) + case "split" => arity(2); split(cols(0), litStr(astArgs(1))) + // **************************************************************// + + // *************** MATH BLOCK ************************************// + case "abs" => arity(1); abs(cols(0)) + case "ceil" | "ceiling" => arity(1); ceil(cols(0)) + case "floor" => arity(1); floor(cols(0)) + case "round" => + arityBetween(1, 2) + if (cols.length == 2) round(cols(0), litInt(astArgs(1))) else round(cols(0)) + case "sqrt" => arity(1); sqrt(cols(0)) + case "pmod" => arity(2); pmod(cols(0), cols(1)) + case "cbrt" => arity(1); cbrt(cols(0)) + case "pow" | "power" => arity(2); pow(cols(0), cols(1)) + case "exp" => arity(1); exp(cols(0)) + case "ln" | "log" => arity(1); log(cols(0)) // Spark `log` = natural log + case "log10" => arity(1); log10(cols(0)) + case "log2" => arity(1); log2(cols(0)) + case "sign" | "signum" => arity(1); signum(cols(0)) + case "greatest" => arityAtLeast(2); greatest(cols: _*) + case "least" => arityAtLeast(2); least(cols: _*) + // **************************************************************// + + // *************** JSON BLOCK ************************************// + case "get_json_object" => arity(2); get_json_object(cols(0), litStr(astArgs(1))) + case "to_json" => arity(1); to_json(cols(0)) + // **************************************************************// + + // *************** XML XPATH BLOCK *******************************// + // NB: Spark's `xpath_*` take (Column, Column), not (Column, String) -- so both args + // are ordinary columns here, no lit-arg extraction needed. + case "xpath_string" => arity(2); xpath_string(cols(0), cols(1)) + case "xpath_boolean" => arity(2); xpath_boolean(cols(0), cols(1)) + case "xpath_short" => arity(2); xpath_short(cols(0), cols(1)) + case "xpath_int" => arity(2); xpath_int(cols(0), cols(1)) + case "xpath_long" => arity(2); xpath_long(cols(0), cols(1)) + case "xpath_float" => arity(2); xpath_float(cols(0), cols(1)) + case "xpath_double" => arity(2); xpath_double(cols(0), cols(1)) + case "xpath" => arity(2); xpath(cols(0), cols(1)) + // **************************************************************// + + // *************** NULL / CONDITIONAL BLOCK **********************// + case "coalesce" => arityAtLeast(1); coalesce(cols: _*) + case "nullif" => arity(2); nullif(cols(0), cols(1)) + case "nvl" | "ifnull" => arity(2); ifnull(cols(0), cols(1)) + // **************************************************************// + + // ************************** HASH ******************************// + case "hash" => arityAtLeast(1); hash(cols: _*) // signed 32-bit murmur3 + case "xxhash64" => arityAtLeast(1); xxhash64(cols: _*) // signed 64-bit + case "md5" => arity(1); md5(cols(0)) + case "sha1" => arity(1); sha1(cols(0)) + case "crc32" => arity(1); crc32(cols(0)) + case "sha2" => arity(2); sha2(cols(0), litInt(astArgs(1))) + // **************************************************************// + + case other => + throw new UnsupportedOperationException( + s"Unsupported function '$other'. Supported: ${supported.mkString(", ")}") + } + } + + // ------------------------------------------------------------------------- + // Literal-typed argument extraction. + // + // Some Spark builtins take a `String`/`Int` literal rather than a `Column` + // (e.g. `get_json_object(col, path: String)`, `round(col, scale: Int)`, + // `regexp_extract(col, pattern: String, idx: Int)`). The lowered `Column` + // cannot carry that, so we read from the AST `Literal` node instead. A + // non-literal passed where a literal is required fails fast. + // ------------------------------------------------------------------------- + + private def litStr(e: Expression): String = e match { + case Literal(s: String) => s + case _ => + throw new UnsupportedOperationException( + "argument must be a string literal (e.g. a JSON path or regex pattern)") + } + + private def litInt(e: Expression): Int = e match { + case Literal(v: Long) => v.toInt + case Literal(v: Int) => v + case _ => + throw new UnsupportedOperationException("argument must be an integer literal") + } + + // ------------------------------------------------------------------------- + // Supported-function lists, one block per family. `supported` concatenates + // them so the error message enumerates every accepted name (every alias is + // listed, matching the convention that each alias is independently valid). + // ------------------------------------------------------------------------- + + private val supportedDTFunctions: Seq[String] = Seq( + "year", + "month", + "day", + "dayofmonth", + "hour", + "minute", + "second", + "quarter", + "dayofweek", + "dayofyear", + "weekofyear", + "date", + "to_date", + "to_timestamp", + "datediff", + "months_between", + "date_add", + "date_sub", + "add_months", + "current_date", + "current_timestamp") + + private val supportedStringFunctions: Seq[String] = Seq( + "lower", + "upper", + "trim", + "ltrim", + "rtrim", + "length", + "substr", + "substring", + "concat", + "regexp_replace", + "regexp_extract", + "rlike", + "regexp_like", + "contains", + "startswith", + "endswith", + "instr", + "split") + + private val supportedMathFunctions: Seq[String] = Seq( + "abs", + "ceil", + "ceiling", + "floor", + "round", + "sqrt", + "cbrt", + "pow", + "power", + "pmod", + "exp", + "ln", + "log", + "log10", + "log2", + "sign", + "signum", + "greatest", + "least") + + private val supportedJsonFunctions: Seq[String] = Seq("get_json_object", "to_json") + + private val supportedXmlFunctions: Seq[String] = Seq( + "xpath_string", + "xpath_boolean", + "xpath_short", + "xpath_int", + "xpath_long", + "xpath_float", + "xpath_double", + "xpath") + + private val supportedNullFunctions: Seq[String] = Seq("coalesce", "nullif", "nvl", "ifnull") + + private val supportedHashFunctions: Seq[String] = + Seq("hash", "xxhash64", "md5", "sha1", "crc32", "sha2") + + private def supported: Seq[String] = + supportedDTFunctions ++ + supportedStringFunctions ++ + supportedMathFunctions ++ + supportedJsonFunctions ++ + supportedXmlFunctions ++ + supportedNullFunctions ++ + supportedHashFunctions +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/GqlAst.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/GqlAst.scala new file mode 100644 index 000000000..de4458455 --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/GqlAst.scala @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +/** + * Hand-written AST for the GQL subset. This is the firewall between ANTLR-generated parse-tree + * types (which appear only inside `AstBuilder`) and the rest of the engine. Nothing in `GqlAst` + * references an ANTLR `*Context` type. + */ +private[propertygraph] sealed trait GqlStatement + +/** The single statement form supported is `MATCH [WHERE ...] [RETURN ...]`. */ +private[propertygraph] final case class MatchStatement( + pattern: GraphPattern, + where: Option[Expression], + returnClause: Option[ReturnClause]) + extends GqlStatement + +/** A linear chain of `NodePattern` / `EdgePattern` elements as written by the user. */ +private[propertygraph] final case class GraphPattern(elements: Seq[PatternElement]) + +private[propertygraph] sealed trait PatternElement + +/** `(variable?:label?)`. Both fields optional: `(a:Person)`, `(x)`, `()`, `(:Person)`. */ +private[propertygraph] final case class NodePattern( + variable: Option[String], + label: Option[String]) + extends PatternElement { + + override def toString: String = { + val innerPart = variable.getOrElse("") + label.map(f => s":$f").getOrElse("") + s"($innerPart)" + } +} + +/** + * `-[variable?:label?]->` or `<-[variable?:label?]-`. Directed, undirected edges. Variable-length + * edges. + */ +private[propertygraph] final case class EdgePattern( + variable: Option[String], + label: Option[String], + direction: Direction, + hopsRange: Option[(Int, Int)]) + extends PatternElement { + + override def toString: String = { + val varPart = variable.map(v => s"$v").getOrElse("") + val labelPart = label.map(l => s":$l").getOrElse("") + val edgeCore = s"$varPart$labelPart" + val hopsPart = hopsRange match { + case Some((lo, hi)) if lo == hi && lo == 1 => "" + case Some((lo, hi)) if lo == hi => s"*$lo" + case Some((lo, hi)) => s"*$lo..$hi" + case None => "" + } + val edgeStr = s"-[$edgeCore$hopsPart]-" + direction match { + case LeftToRight => s"$edgeStr>" + case RightToLeft => s"<$edgeStr" + case Undirected => edgeStr + } + } +} +private[propertygraph] sealed trait Direction +private[propertygraph] case object LeftToRight extends Direction // `-[e]->` +private[propertygraph] case object RightToLeft extends Direction // `<-[e]-` +private[propertygraph] case object Undirected extends Direction // `-[e]-` + +// --------------------------------------------------------------------------- +// RETURN clause +// --------------------------------------------------------------------------- + +private[propertygraph] sealed trait ReturnClause +private[propertygraph] case object ReturnStar extends ReturnClause +private[propertygraph] final case class ReturnItems(items: Seq[ReturnItem]) extends ReturnClause + +private[propertygraph] final case class ReturnItem(expression: Expression, alias: Option[String]) + +// --------------------------------------------------------------------------- +// Expressions (shared by WHERE and RETURN). +// +// Precedence mirrors the grammar: OR < AND < NOT < comparison < additive < +// multiplicative < primary. `Comparison` is non-chained (single operator). +// `Arithmetic` covers additive (`+`/`-`) and multiplicative (`*`/`/`/`%`) ops; +// precedence is encoded in the grammar tier, not in the node. +// --------------------------------------------------------------------------- + +private[propertygraph] sealed trait Expression + +private[propertygraph] final case class Literal(value: Any) extends Expression +private[propertygraph] final case class Variable(name: String) extends Expression +private[propertygraph] final case class PropertyAccess(variable: String, property: String) + extends Expression +private[propertygraph] final case class Comparison( + left: Expression, + op: CompOp, + right: Expression) + extends Expression +private[propertygraph] final case class Arithmetic( + left: Expression, + op: ArithOp, + right: Expression) + extends Expression +private[propertygraph] final case class Not(expr: Expression) extends Expression +private[propertygraph] final case class And(left: Expression, right: Expression) + extends Expression +private[propertygraph] final case class Or(left: Expression, right: Expression) extends Expression +private[propertygraph] final case class FunctionCall(name: String, args: Seq[Expression]) + extends Expression + +private[propertygraph] sealed trait CompOp +private[propertygraph] case object Eq extends CompOp // `=` +private[propertygraph] case object Neq extends CompOp // `<>` or `!=` +private[propertygraph] case object Lt extends CompOp // `<` +private[propertygraph] case object Lte extends CompOp // `<=` +private[propertygraph] case object Gt extends CompOp // `>` +private[propertygraph] case object Gte extends CompOp // `>=` + +// Arithmetic operators. `AddOp` (+/-) and `MulOp` (*///%) are subtraits of a common +// `ArithOp` so the `Arithmetic` node carries one op type; precedence is encoded in the grammar +// (multiplicative binds tighter than additive), not here. +private[propertygraph] sealed trait ArithOp +private[propertygraph] sealed trait AddOp extends ArithOp +private[propertygraph] case object Plus extends AddOp // `+` +private[propertygraph] case object Minus extends AddOp // `-` +private[propertygraph] sealed trait MulOp extends ArithOp +private[propertygraph] case object Mult extends MulOp // `*` +private[propertygraph] case object Div extends MulOp // `/` (floating-point division) +private[propertygraph] case object Mod extends MulOp // `%` (sign follows the dividend) + +private[propertygraph] object GqlAst { + + /** Collect every variable name referenced anywhere in `expr`. */ + def referencedVariables(expr: Expression): Set[String] = expr match { + case Variable(name) => Set(name) + case PropertyAccess(variable, _) => Set(variable) + case Literal(_) => Set.empty + case Comparison(l, _, r) => referencedVariables(l) ++ referencedVariables(r) + case Arithmetic(l, _, r) => referencedVariables(l) ++ referencedVariables(r) + case Not(e) => referencedVariables(e) + case And(l, r) => referencedVariables(l) ++ referencedVariables(r) + case Or(l, r) => referencedVariables(l) ++ referencedVariables(r) + case FunctionCall(_, args) => args.flatMap(referencedVariables).toSet + } + + /** + * Split a boolean expression on top-level `AND` into its conjuncts. Used by the resolver to + * classify WHERE predicates individually (scan-local vs join vs post-join). + */ + def flattenAnd(expr: Expression): Seq[Expression] = expr match { + case And(l, r) => flattenAnd(l) ++ flattenAnd(r) + case other => Seq(other) + } +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/GqlExplain.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/GqlExplain.scala new file mode 100644 index 000000000..940a7a3dc --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/GqlExplain.scala @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +/** + * Read-only renderers over the two intermediate IR values: + * - [[logical]] renders a [[ResolvedQuery]] (the resolved schema paths, WHERE classification, + * and projection); + * - [[physical]] renders a `Seq[JoinPlan]` (per plan: the path, element-level join order, the + * statistics that drove it, and the predicates). + * + * Both are pure JVM; neither touches Spark. + */ +private[propertygraph] object GqlExplain { + + /** Render the logical (resolved) plan. */ + def logical(query: ResolvedQuery): String = { + val b = Vector.newBuilder[String] + b += "Logical plan (resolved):" + + if (query.paths.isEmpty) { + b += " schema paths: (none -- pattern is disconnected in the schema graph)" + } else { + b += s" schema paths (${query.paths.size}):" + query.paths.zipWithIndex.foreach { case (p, idx) => + b += s" [$idx] ${renderPath(p)}" + } + } + + b += s" join predicates (${query.joinPredicates.size}):" + query.joinPredicates.foreach(e => b += s" - ${renderExpr(e)}") + b += s" post-join filters (${query.postFilters.size}):" + query.postFilters.foreach(e => b += s" - ${renderExpr(e)}") + b += s" projection: ${renderProjection(query.projection)}" + + b.result().mkString("\n") + } + + /** Render the physical plan (one block per JoinPlan). */ + def physical(plans: Seq[JoinPlan]): String = { + val b = Vector.newBuilder[String] + b += s"Physical plan (${plans.size} join plan(s)):" + if (plans.isEmpty) { + b += " (none -- pattern is disconnected in the schema graph; no Spark execution)" + } else { + plans.zipWithIndex.foreach { case (plan, idx) => + b += s" Plan $idx:" + b += s" path: ${renderPath(plan.path)}" + b += s" join order: ${renderOrder(plan.order)}" + b += s" statistics: ${renderStats(plan.statsUsed)}" + b += s" join predicates (${plan.joinPredicates.size}):" + plan.joinPredicates.foreach(e => b += s" - ${renderExpr(e)}") + b += s" post-join filters (${plan.postFilters.size}):" + plan.postFilters.foreach(e => b += s" - ${renderExpr(e)}") + b += s" projection: ${renderProjection(plan.projection)}" + } + } + b.result().mkString("\n") + } + + // --------------------------------------------------------------------- + // Renderers. + // --------------------------------------------------------------------- + + private def renderPath(path: SchemaPath): String = { + // (a:Person)-[e1:KNOWS]->(b:Person)<-[e2:LIKES]-(c:Person) + val sb = new StringBuilder + path.nodes.zipWithIndex.foreach { case (node, i) => + sb.append(renderNode(node)) + if (i < path.steps.length) { + sb.append(renderStep(path.steps(i))) + } + } + sb.toString + } + + private def renderNode(node: PathNode): String = { + val v = node.variable.getOrElse("_") + val filter = + if (node.scanFilter.isEmpty) "" + else node.scanFilter.map(renderExpr).mkString("{", " AND ", "}") + s"($v:${node.vertexGroupName}$filter)" + } + + private def renderStep(step: PathStep): String = { + val v = step.variable.map(n => s"$n:").getOrElse("") + val body = s"[$v${step.edge.edgeGroupName}]" + // Forward step: -(body)-> ; backward step: <-(body)- + if (step.traversedForward) s"-$body->" else s"<-$body-" + } + + private def renderOrder(order: Vector[PathElementRef]): String = + order + .map { + case NodeRef(i) => s"n$i" + case EdgeRef(i) => s"e$i" + } + .mkString("[", ", ", "]") + + private def renderStats(stats: Map[String, GroupStatistics]): String = + if (stats.isEmpty) "(no statistics)" + else + stats.toVector + .sortBy(_._1) + .map { case (k, v) => + s"$k=${v.rowCount.map(c => s"rows=$c").getOrElse("?")}" + } + .mkString("{", ", ", "}") + + private def renderProjection(projection: Projection): String = projection match { + case Projection.Default => "(default: matched IDs of first/last named nodes)" + case Projection.Star => "* (all matched variables)" + case Projection.Items(items) => + items + .map { it => + val core = renderExpr(it.expression) + it.alias match { + case Some(a) => s"$core AS $a" + case None => core + } + } + .mkString("items: [", ", ", "]") + } + + private def renderExpr(expr: Expression): String = expr match { + case Literal(value) => + value match { + case null => "NULL" + case s: String => s"'$s'" + case other => String.valueOf(other) + } + case Variable(name) => name + case PropertyAccess(variable, property) => s"$variable.$property" + case Comparison(left, op, right) => + s"(${renderExpr(left)} ${renderCompOp(op)} ${renderExpr(right)})" + case Arithmetic(left, op, right) => + s"(${renderExpr(left)} ${renderArithOp(op)} ${renderExpr(right)})" + case Not(e) => s"(NOT ${renderExpr(e)})" + case And(left, right) => s"(${renderExpr(left)} AND ${renderExpr(right)})" + case Or(left, right) => s"(${renderExpr(left)} OR ${renderExpr(right)})" + case FunctionCall(name, args) => + s"$name(${args.map(renderExpr).mkString(", ")})" + } + + private def renderCompOp(op: CompOp): String = op match { + case Eq => "=" + case Neq => "<>" + case Lt => "<" + case Lte => "<=" + case Gt => ">" + case Gte => ">=" + } + + private def renderArithOp(op: ArithOp): String = op match { + case Plus => "+" + case Minus => "-" + case Mult => "*" + case Div => "/" + case Mod => "%" + } +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/GraphStatistics.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/GraphStatistics.scala new file mode 100644 index 000000000..8771d0f5f --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/GraphStatistics.scala @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +/** + * Per-column statistics for a property group. All fields optional and additive: future statistics + * providers can fill more of them without changing callers or the path/plan types. + * + * @param distinctCount + * estimated number of distinct values, if known. + * @param nullCount + * number of nulls, if known. + * @param min + * minimum value, if known. + * @param max + * maximum value, if known. + */ +private[propertygraph] final case class ColumnStatistics( + distinctCount: Option[Long] = None, + nullCount: Option[Long] = None, + min: Option[Any] = None, + max: Option[Any] = None) + +/** + * Per-group statistics (a vertex property group or an edge property group). + * + * @param rowCount + * number of rows in the group, if known. + * @param sizeInBytes + * estimated on-disk / in-memory size, if known. + * @param columns + * per-column stats, keyed by column name. + */ +private[propertygraph] final case class GroupStatistics( + rowCount: Option[Long] = None, + sizeInBytes: Option[Long] = None, + columns: Map[String, ColumnStatistics] = Map.empty) + +/** + * Statistics source SPI. The optimizer queries it by group name; the *source* is pluggable so + * that future implementations (Parquet footer min/max, managed-table CBO, …) can be swapped in + * with no change to callers or to the path/plan types. + */ +private[propertygraph] trait GraphStatistics { + + /** Statistics for the named vertex property group; empty stats if unknown. */ + def vertexGroup(name: String): GroupStatistics + + /** Statistics for the named edge property group; empty stats if unknown. */ + def edgeGroup(name: String): GroupStatistics +} + +private[propertygraph] object GraphStatistics { + + /** A no-op provider returning empty stats for every group. */ + val Empty: GraphStatistics = new GraphStatistics { + override def vertexGroup(name: String): GroupStatistics = GroupStatistics() + override def edgeGroup(name: String): GroupStatistics = GroupStatistics() + } + + /** + * Build a provider that caches `rowCount` per group on first access by calling `df.count()`. + * The cache is built lazily and shared across `vertexGroup`/`edgeGroup` lookups. All + * non-rowCount fields stay empty. + */ + def cachedRowCount( + vertexRowCounts: Map[String, Long], + edgeRowCounts: Map[String, Long]): GraphStatistics = new GraphStatistics { + override def vertexGroup(name: String): GroupStatistics = + vertexRowCounts + .get(name) + .map(c => GroupStatistics(rowCount = Some(c))) + .getOrElse(GroupStatistics()) + override def edgeGroup(name: String): GroupStatistics = + edgeRowCounts + .get(name) + .map(c => GroupStatistics(rowCount = Some(c))) + .getOrElse(GroupStatistics()) + } +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/JoinOptimizer.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/JoinOptimizer.scala new file mode 100644 index 000000000..30b2e350d --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/JoinOptimizer.scala @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +/** + * Turns a [[ResolvedQuery]] into a sequence of [[JoinPlan]]s (the physical plan). This is the + * optimization boundary: it is the only place where join order is decided and where statistics + * are (optionally) consumed. + * + * Disconnected patterns (`query.paths.isEmpty`) yield no plans; the executor then produces an + * empty result DataFrame. + */ +private[propertygraph] object JoinOptimizer { + + /** + * Default entry point. + */ + def plan(query: ResolvedQuery, stats: Option[GraphStatistics]): Seq[JoinPlan] = + defaultPlanner(query, stats) + + /** + * Planner SPI: `ResolvedQuery x Option[GraphStatistics] => Seq[JoinPlan]`. + */ + type Planner = (ResolvedQuery, Option[GraphStatistics]) => Seq[JoinPlan] + + /** + * Refinement SPI applied after the default planner: `(ResolvedQuery, Seq[JoinPlan]) => + * Seq[JoinPlan]`. Lets handwritten rules reorder / prune plans without replacing the planner. + */ + type PlanRefiner = (ResolvedQuery, Seq[JoinPlan]) => Seq[JoinPlan] + + /** planner: pattern order, no statistics consumption. */ + val defaultPlanner: Planner = (query, _) => patternOrderPlans(query) + + /** refiner: identity (no-op). */ + val identityRefiner: PlanRefiner = (_, plans) => plans + + /** + * Build one `JoinPlan` per path, joining elements in the order they were written (`n0, e0, n1, + * e1, …, n_{k}`). `statsUsed` is empty; predicates/projection are carried through from the + * resolved query. + */ + private def patternOrderPlans(query: ResolvedQuery): Seq[JoinPlan] = + query.paths.map { path => + val order = patternOrder(path) + JoinPlan( + path = path, + order = order, + statsUsed = Map.empty, + joinPredicates = query.joinPredicates, + postFilters = query.postFilters, + projection = query.projection) + } + + /** `n0, e0, n1, e1, …, n_{k}` for a path with `k` steps (`k+1` nodes). */ + private[propertygraph] def patternOrder(path: SchemaPath): Vector[PathElementRef] = { + val b = Vector.newBuilder[PathElementRef] + b += NodeRef(0) + path.steps.indices.foreach { i => + b += EdgeRef(i) + b += NodeRef(i + 1) + } + b.result() + } +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/JoinPlan.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/JoinPlan.scala new file mode 100644 index 000000000..26f413dd4 --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/JoinPlan.scala @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +/** + * A reference to one element of a [[SchemaPath]] within a [[JoinPlan]]'s join order. The order is + * expressed at the granularity of individual nodes and edges (not step indices). + */ +private[propertygraph] sealed trait PathElementRef +private[propertygraph] final case class NodeRef(index: Int) extends PathElementRef { + require(index >= 0) +} +private[propertygraph] final case class EdgeRef(index: Int) extends PathElementRef { + require(index >= 0) +} + +/** + * The physical plan for one [[SchemaPath]]. Self-contained: it carries everything the executor + * needs, so `explain(physical)` can render path + order + the statistics that drove it without + * re-running resolution. + * + * @param path + * the resolved schema path this plan executes (topology + per-node scan filters + directions). + * @param order + * element-level join order: a sequence of [[NodeRef]] / [[EdgeRef]] into `path.nodes` / + * `path.steps`. The executor scans and joins in this order. + * @param statsUsed + * the per-group statistics that drove `order`. + * @param joinPredicates + * WHERE conjuncts spanning exactly two adjacent node variables; applied as join conditions. + * @param postFilters + * WHERE conjuncts spanning 3+ variables / non-adjacent / any edge variable; applied after the + * join tree. + * @param projection + * the RETURN shape (Default / Star / Items). + */ +private[propertygraph] final case class JoinPlan( + path: SchemaPath, + order: Vector[PathElementRef], + statsUsed: Map[String, GroupStatistics], + joinPredicates: Seq[Expression], + postFilters: Seq[Expression], + projection: Projection) { + + override def toString: String = { + val orderStr = order + .map { + case NodeRef(i) => s"n$i" + case EdgeRef(i) => s"e$i" + } + .mkString("[", ", ", "]") + val projStr = projection match { + case Projection.Default => "DEFAULT" + case Projection.Star => "*" + case Projection.Items(items) => items.mkString("Items(", ", ", ")") + } + s"JoinPlan(path=$path, order=$orderStr, projection=$projStr)" + } +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/QueryExecutor.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/QueryExecutor.scala new file mode 100644 index 000000000..bdbddd98b --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/QueryExecutor.scala @@ -0,0 +1,712 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +import org.apache.spark.sql.Column +import org.apache.spark.sql.DataFrame +import org.apache.spark.sql.SparkSession +import org.apache.spark.sql.functions.array +import org.apache.spark.sql.functions.col +import org.apache.spark.sql.functions.lit +import org.apache.spark.sql.functions.struct +import org.apache.spark.sql.types.ArrayType +import org.apache.spark.sql.types.StringType +import org.apache.spark.sql.types.StructField +import org.apache.spark.sql.types.StructType +import org.graphframes.GraphFrame +import org.graphframes.propertygraph.PropertyGraphFrame + +/** + * Executes a sequence of [[JoinPlan]]s against a [[PropertyGraphFrame]] and returns a single + * result DataFrame. + * + * Per plan: build a join tree following `plan.order` (each element scanned once and joined to the + * growing frame on the masked id columns), apply join/post predicates, project to the output + * schema. Across plans: `UNION ALL`. Reuses `PropertyGroup.getData(filter, requestedProperties)`, + * which pushes the scan-local filter, applies id masking, and projects standardized columns -- + * edge `src`/`dst` are masked the same way as vertex `id`, so joins line up with no manual + * casting. + */ +private[propertygraph] object QueryExecutor { + + // ------------------------------------------------------------------------- + // Output column-name constants. Package-private so test suites can reference + // them when asserting against the query result schema/rows instead of + // repeating the raw string literals. + // ------------------------------------------------------------------------- + private[propertygraph] val EDGE_PROPERTY_GROUP: String = "edge_property_group" + private[propertygraph] val NODE_ID: String = "node_id" + private[propertygraph] val NODE_PROPERTY_GROUP: String = "node_property_group" + private[propertygraph] val START_ID: String = "start_id" + private[propertygraph] val START_PROPERTY_GROUP: String = "start_property_group" + private[propertygraph] val END_ID: String = "end_id" + private[propertygraph] val END_PROPERTY_GROUP: String = "end_property_group" + private[propertygraph] val PATH: String = "path" + + /** + * Execute all plans and `UNION ALL` them. + * + * Empty plans (disconnected pattern) -> an empty DataFrame with the fixed output schema (see + * [[outputSchema]]). Empty result of a single plan is also a valid empty DataFrame. + * + * Scan sharing: a per-call memo (`scanMemo`) de-duplicates scans across the per-path fan-out. + * It is keyed by a canonical scan signature ([[ScanKey]]) so every plan that references the + * same group with the same scan-local filter and the same carried-column set pulls the *same* + * `DataFrame` reference. This is the "floor" of scan reuse: never construct the same scan + * twice. + */ + def execute(pgf: PropertyGraphFrame, plans: Seq[JoinPlan]): DataFrame = { + if (plans.isEmpty) { + val spark = pgf.vertexPropertyGroups.headOption + .map(_.data.sparkSession) + .orElse(pgf.edgesPropertyGroups.headOption.map(_.data.sparkSession)) + .orElse(SparkSession.getActiveSession) + .getOrElse( + throw new IllegalStateException( + "No active SparkSession and no property groups to derive one from; " + + "PropertyGraphFrame.query must be called with an active session")) + return spark.createDataFrame( + spark.sparkContext.emptyRDD[org.apache.spark.sql.Row], + outputSchema) + } + val scanMemo = scala.collection.mutable.Map.empty[ScanKey, DataFrame] + val perPlan = plans.map(executePlan(pgf, _, scanMemo)) + perPlan.reduce(_ unionByName _) + } + + /** + * Test-only (debug) variant of [[execute]] that also returns the per-call scan memo, so tests + * can assert the scan-reuse "floor" (a scan is never constructed twice for an equal + * [[ScanKey]]) by reference-identity on the memo values. Package-private; not part of any + * public contract. + */ + private[propertygraph] def executeWithScanMemo( + pgf: PropertyGraphFrame, + plans: Seq[JoinPlan]): (DataFrame, Map[ScanKey, DataFrame]) = { + val scanMemo = scala.collection.mutable.Map.empty[ScanKey, DataFrame] + val result = + if (plans.isEmpty) execute(pgf, plans) + else { + val perPlan = plans.map(executePlan(pgf, _, scanMemo)) + perPlan.reduce(_ unionByName _) + } + (result, scanMemo.toMap) + } + + /** + * Canonical signature of a scan. Two scans with equal keys share one `DataFrame` in the memo. + * `Expression` AST nodes (including the scan-local filters) are case classes with structural + * equality, so they key correctly without extra canonicalization. `carriedCols` is a `Set`, so + * column order does not affect sharing. + */ + private[propertygraph] final case class ScanKey( + groupName: String, + scanFilter: Seq[Expression], + carriedCols: Set[String]) + + // ------------------------------------------------------------------------- + // Per-plan execution. + // ------------------------------------------------------------------------- + + private def executePlan( + pgf: PropertyGraphFrame, + plan: JoinPlan, + scanMemo: scala.collection.mutable.Map[ScanKey, DataFrame]): DataFrame = { + val path = plan.path + val env = PrefixEnv(path) + + // Classify, per element, which property columns are CARRIED through the scan (predicate / + // filter-also-returned columns that influence join cardinality) vs which are OUTPUT-ONLY + // (referenced solely by RETURN, deferred to a terminal join-back). + val nodeProps: Map[Int, ElementProps] = classifyElementProps(path, plan, node = true) + val edgeProps: Map[Int, ElementProps] = classifyElementProps(path, plan, node = false) + + // Scan each element once (shared via the memo below the rename) and alias its columns under the + // element's prefix. The expensive shared node sits below a thin per-use Project (the rename). + val nodeFrames: Map[Int, DataFrame] = path.nodes.indices.map { i => + val shared = + sharedScanNode(pgf, path, i, nodeProps.getOrElse(i, ElementProps.Empty), scanMemo) + i -> renameAll(shared, env.nodePrefix(i)) + }.toMap + val edgeFrames: Map[Int, DataFrame] = path.steps.indices.map { i => + val shared = + sharedScanEdge(pgf, path, i, edgeProps.getOrElse(i, ElementProps.Empty), scanMemo) + i -> renameAll(shared, env.edgePrefix(i)) + }.toMap + + // Walk the join order, joining each element onto the growing frame, placing every multi-variable + // predicate at the earliest element where all its operands are bound (replacing a blanket + // post-tree `.where`). + val allPredicates: Seq[Expression] = plan.joinPredicates ++ plan.postFilters + val predicateVarSets: Seq[Set[String]] = allPredicates.map(GqlAst.referencedVariables) + val placed = scala.collection.mutable.BitSet.empty + var frame: DataFrame = null + var boundVars: Set[String] = Set.empty + def bindElement(i: Int, isNode: Boolean): Unit = { + if (isNode) path.nodes(i).variable.foreach(v => boundVars += v) + else path.steps(i).variable.foreach(v => boundVars += v) + } + plan.order.foreach { + case NodeRef(i) => + val f = nodeFrames(i) + bindElement(i, isNode = true) + if (frame == null) { + frame = f + } else { + val ready = readyPredicateIndices(predicateVarSets, placed, boundVars) + val readyExprs = ready.map(allPredicates) + frame = joinElement(frame, f, path, env, readyExprs) + ready.foreach(placed.add) + } + case EdgeRef(i) => + val f = edgeFrames(i) + bindElement(i, isNode = false) + if (frame == null) { + frame = f + } else { + val ready = readyPredicateIndices(predicateVarSets, placed, boundVars) + val readyExprs = ready.map(allPredicates) + frame = joinElement(frame, f, path, env, readyExprs) + ready.foreach(placed.add) + } + } + if (frame == null) { + // Degenerate: a path with a single node and no edges (MATCH (a:Person)). + // Synthesize a trivially-valid frame from that node scan; nodeFrames is non-empty here. + frame = nodeFrames(0) + } + // Place any predicates not consumed during the join walk (e.g. a predicate whose variables were + // all bound by the seed element, or a literal-only predicate) as a residual filter. + val leftover = allPredicates.indices.filterNot(placed.contains).map(allPredicates) + leftover.foreach { expr => + frame = frame.filter(ExpressionLowering.lower(expr, env)) + } + + // edgeProps is classified (and consumed by the shared edge scans above) but NOT join-backed + // edge groups may be undirected (doubling rows), so edge RETURN-properties are CARRIED rather + // than resolved by an id-keyed join-back. + project(frame, plan, nodeProps, pgf) + } + + /** Indices of predicates whose referenced variables are all bound and not yet placed. */ + private def readyPredicateIndices( + varSets: Seq[Set[String]], + placed: scala.collection.mutable.BitSet, + boundVars: Set[String]): Seq[Int] = + varSets.indices.collect { + case k if !placed.contains(k) && varSets(k).subsetOf(boundVars) => k + } + + // ------------------------------------------------------------------------- + // Scans + aliasing. + // ------------------------------------------------------------------------- + + /** + * Return the shared (memoized), *un-prefixed* node scan for element `i`. The caller applies the + * per-element rename on top. `ElementProps.carriedToScan` is what the scan requests; + * output-only columns are NOT requested here (they are resolved by the terminal join-back in + * `project`). + */ + private def sharedScanNode( + pgf: PropertyGraphFrame, + path: SchemaPath, + i: Int, + props: ElementProps, + scanMemo: scala.collection.mutable.Map[ScanKey, DataFrame]): DataFrame = { + val node = path.nodes(i) + val key = ScanKey(node.vertexGroupName.toLowerCase, node.scanFilter, props.carriedToScan) + scanMemo.getOrElseUpdate( + key, { + val group = pgf.vertexGroups(node.vertexGroupName.toLowerCase) + val filterCol = lowerScanFilter(node.scanFilter) + group.getData(filterCol, props.carriedToScan.toSeq.sorted) + }) + } + + /** + * Return the shared (memoized), *un-prefixed* edge scan for element `i`. + */ + private def sharedScanEdge( + pgf: PropertyGraphFrame, + path: SchemaPath, + i: Int, + props: ElementProps, + scanMemo: scala.collection.mutable.Map[ScanKey, DataFrame]): DataFrame = { + val step = path.steps(i) + val key = ScanKey(step.edge.edgeGroupName.toLowerCase, step.scanFilter, props.carriedToScan) + scanMemo.getOrElseUpdate( + key, { + val group = pgf.edgeGroups(step.edge.edgeGroupName.toLowerCase) + val filterCol = lowerScanFilter(step.scanFilter) + group.getData(filterCol, props.carriedToScan.toSeq.sorted) + }) + } + + /** + * Lower the scan-local predicates of a node into a single ANDed Column (lit(true) if none). + * These reference the node's variable, but the resulting Column is applied by `getData` against + * the RAW group columns (before aliasing), so the variable resolves to the empty prefix (raw + * column names): e.g. `a.age > 30` -> `col("age") > 30`. + */ + private def lowerScanFilter(filters: Seq[Expression]): Column = + if (filters.isEmpty) lit(true) + else filters.map(ExpressionLowering.lower(_, PrefixEnv.raw)).reduce(_ && _) + + /** Prefix every column of `df` with `prefix_` in place. */ + private def renameAll(df: DataFrame, prefix: String): DataFrame = { + val mapping = df.columns.map(c => c -> s"${prefix}_$c").toMap + val out = df.withColumnsRenamed(mapping) + out + } + + // ------------------------------------------------------------------------- + // Join tree. + // ------------------------------------------------------------------------- + + /** + * Join `incoming` onto `frame`. The join condition is the conjunction of all adjacency edges + * between the just-added element and already-present neighbors. + * + * For a step `k` connecting `node_k` and `node_{k+1}` through `edge_k`: + * - forward (`traversedForward = true`): `edge_k.src == node_k.id` and + * `edge_k.dst == node_{k+1}.id`; + * - backward: `edge_k.dst == node_k.id` and `edge_k.src == node_{k+1}.id` (src/dst swapped). + */ + private def joinElement( + frame: DataFrame, + incoming: DataFrame, + path: SchemaPath, + env: PrefixEnv, + residualPredicates: Seq[Expression]): DataFrame = { + val presentCols = frame.columns.toSet + val conditions = adjacencyConditions(incoming.columns.toSet, presentCols, path, env) + require( + conditions.nonEmpty, + "Join order produced an element with no adjacency to the already-joined frame; " + + "this indicates an invalid join order" + s" for path ${path.toString()}") + // The residual predicates are multi-variable WHERE conjuncts whose operands are all bound at + // this join. + val residualCols = residualPredicates.map(ExpressionLowering.lower(_, env)) + val allConds = conditions ++ residualCols + frame.join(incoming, allConds.reduce(_ && _), "inner") + } + + /** + * Build the equi-join conditions between the newly-joined element's columns and the frame's + * columns. An edge `k` is adjacent to its two endpoint nodes (`k`, `k+1`); a node `j` is + * adjacent to the edges touching it (`j-1` and `j`). + */ + private def adjacencyConditions( + incoming: Set[String], + present: Set[String], + path: SchemaPath, + env: PrefixEnv): Seq[Column] = { + val conds = Seq.newBuilder[Column] + + def bothPresent(a: String, b: String): Option[Column] = + if (incoming.contains(a) && present.contains(b)) Some(col(a) === col(b)) + else if (incoming.contains(b) && present.contains(a)) Some(col(b) === col(a)) + else None + + // Edge k <-> node k and edge k <-> node k+1. + path.steps.indices.foreach { k => + val eSrc = env.edgeCol(k, GraphFrame.SRC) + val eDst = env.edgeCol(k, GraphFrame.DST) + val nKId = env.nodeCol(k, GraphFrame.ID) + val nK1Id = env.nodeCol(k + 1, GraphFrame.ID) + val forward = path.steps(k).traversedForward + // forward: edge.src == node_k.id , edge.dst == node_{k+1}.id + // backward: edge.dst == node_k.id , edge.src == node_{k+1}.id + val (srcNode, dstNode) = if (forward) (nKId, nK1Id) else (nK1Id, nKId) + bothPresent(eSrc, srcNode).foreach(conds += _) + bothPresent(eDst, dstNode).foreach(conds += _) + } + conds.result() + } + + /** The fixed output schema, regardless of pattern length or RETURN shape. */ + private[propertygraph] def outputSchema: StructType = { + val pathElement = StructType( + Seq( + StructField(EDGE_PROPERTY_GROUP, StringType, nullable = true), + StructField(NODE_ID, StringType, nullable = true), + StructField(NODE_PROPERTY_GROUP, StringType, nullable = true))) + StructType( + Seq( + StructField(START_ID, StringType, nullable = true), + StructField(START_PROPERTY_GROUP, StringType, nullable = true), + StructField(END_ID, StringType, nullable = true), + StructField(END_PROPERTY_GROUP, StringType, nullable = true), + StructField(EDGE_PROPERTY_GROUP, StringType, nullable = true), + StructField(PATH, ArrayType(pathElement), nullable = true))) + } + + private def project( + frame: DataFrame, + plan: JoinPlan, + nodeProps: Map[Int, ElementProps], + pgf: PropertyGraphFrame): DataFrame = { + val path = plan.path + val env = PrefixEnv(path) + plan.projection match { + case Projection.Items(items) => + // For any node element with output-only (RETURN-only) properties, join the masked id back to + // the group's properties so those columns are available for the RETURN projection. Carried + // columns (predicate / filter-also-returned) are already on `frame` under the element + // prefix; only output-only columns need the terminal join-back. Edges are NOT join-backed + // and edge RETURN-properties are carried instead. + // + // The joined-back property columns are aliased to the element's PREFIXED names so that + // `ExpressionLowering.lower(PropertyAccess(v, p))` -- which resolves to `_p` -- + // finds them. (Carried columns already live under those prefixed names; this puts the + // output-only ones under the same convention.) + var withOutput = frame + path.nodes.indices.foreach { i => + val props = nodeProps.getOrElse(i, ElementProps.Empty) + if (props.outputOnly.nonEmpty) { + val node = path.nodes(i) + val group = pgf.vertexGroups(node.vertexGroupName.toLowerCase) + val prefix = env.nodePrefix(i) + // Build a narrow join-back frame: the masked `id` (join key) plus the output-only + // property columns, renamed to the element's prefixed names so `ExpressionLowering` + // resolves `PropertyAccess(v, p)` -> `_p` against them. We drop + // `property_group` (the group name is a constant the projection emits itself) and avoid + // surfacing a second un-prefixed `id` column that would collide across multiple + // join-backs; the masked `id` is kept only as the join key and dropped afterward. + val carryCols = props.outputOnly.toSeq.sorted + // `_gjid_` is a throwaway join-key alias unique per element, so multiple join-backs + // never collide on a raw `id` column. + val joinKey = s"_gjid_$i" + val groupId = group + .getData(lit(true), carryCols) + .select((col(GraphFrame.ID).alias(joinKey) +: carryCols + .map(p => col(p).alias(env.join(prefix, p)))): _*) + val idCol = env.nodeCol(i, GraphFrame.ID) + withOutput = withOutput.join(groupId, withOutput(idCol) === groupId(joinKey), "left") + } + } + val cols = items.map { item => + val c = ExpressionLowering.lower(item.expression, env) + item.alias match { + case Some(a) => c.alias(a) + case None => + item.expression match { + case Variable(v) => c.alias(v) + case PropertyAccess(_, p) => c.alias(p) + case FunctionCall(name, _) => c.alias(name.toLowerCase) + case _ => c + } + } + } + withOutput.select(cols: _*) + + case Projection.Default | Projection.Star => + val namedIndices = path.nodes.indices.filter(i => path.nodes(i).variable.isDefined) + require( + namedIndices.nonEmpty, + "RETURN (default/*) requires at least one named node variable in the pattern;" + s"path: ${plan.toString()}") + val startIdx = namedIndices.head + val endIdx = namedIndices.last + + val startId = col(env.nodeCol(startIdx, GraphFrame.ID)) + val startPg = lit(path.nodes(startIdx).vertexGroupName) + val endId = col(env.nodeCol(endIdx, GraphFrame.ID)) + val endPg = lit(path.nodes(endIdx).vertexGroupName) + + if (path.steps.isEmpty) { + // 0-hop: a single node. No edge, no path array. + // edge_property_group is null; path is an empty array. + frame.select( + startId.alias(START_ID), + startPg.alias(START_PROPERTY_GROUP), + endId.alias(END_ID), + endPg.alias(END_PROPERTY_GROUP), + lit(null).cast(StringType).alias(EDGE_PROPERTY_GROUP), + array().cast(outputSchema(PATH).dataType).alias(PATH)) + } else { + val firstEdgeGroup = + lit(path.steps.head.edge.edgeGroupName).alias(EDGE_PROPERTY_GROUP) + // Intermediate hops: for step i (i in 1..k-1) we emit the edge group of step i and the + // intermediate node i. The final step k carries its edge group but null node fields (the + // end node is already in end_id). + val pathStructs = path.steps.indices.flatMap { i => + val edgeGroup = lit(path.steps(i).edge.edgeGroupName) + if (i == path.steps.length - 1) { + // Last step: edge group only, null node fields. + Seq( + struct( + edgeGroup.alias(EDGE_PROPERTY_GROUP), + lit(null).cast(StringType).alias(NODE_ID), + lit(null).cast(StringType).alias(NODE_PROPERTY_GROUP))) + } else { + Seq( + struct( + edgeGroup.alias(EDGE_PROPERTY_GROUP), + col(env.nodeCol(i + 1, GraphFrame.ID)).alias(NODE_ID), + lit(path.nodes(i + 1).vertexGroupName).alias(NODE_PROPERTY_GROUP))) + } + } + val pathCol = + if (pathStructs.isEmpty) array().cast(outputSchema(PATH).dataType) + else array(pathStructs: _*) + frame.select( + startId.alias(START_ID), + startPg.alias(START_PROPERTY_GROUP), + endId.alias(END_ID), + endPg.alias(END_PROPERTY_GROUP), + firstEdgeGroup, + pathCol.alias(PATH)) + } + } + } + + // ------------------------------------------------------------------------- + // Required-property classification (carry vs defer). + // + // Per scan-reuse: every required property is classified by the role that + // references it: + // - scan-local filter columns -> pushed into getData's filter, consumed before the shuffle; + // - join / post-filter columns -> CARRIED through the join tree to the predicate's evaluation + // point (they let the predicate cut the join's output cardinality); + // - output-only columns -> DEFERRED to a terminal join-back (they never reduce + // cardinality, so carrying them only widens shuffles and fragments scan signatures). + // A property referenced by BOTH a filter and RETURN is carried (the filter need dominates). + // ------------------------------------------------------------------------- + + /** Per-element property classification. */ + private[propertygraph] final case class ElementProps( + carriedToScan: Set[String], // requested at the scan; rides the join tree to its consumers + outputOnly: Set[String] + ) // RETURN-only; resolved by the terminal join-back, never carried + + private[propertygraph] object ElementProps { + val Empty: ElementProps = ElementProps(Set.empty, Set.empty) + } + + /** + * Classify the properties of every element of the requested kind (node or edge) into carried vs + * output-only. + * + * For an element bound to variable `v`: + * - `scanFilterProps(v)` = props referenced in this element's scan-local filters; + * - `joinPostProps(v)` = props of `v` referenced in join/post predicates; + * - `returnProps(v)` = props of `v` referenced in RETURN items (empty for Default/Star); + * - `carry(v)` = `joinPostProps(v) ∪ (returnProps(v) ∩ scanFilterProps(v))`; + * - `outputOnly(v)` = `returnProps(v) − carry(v)`. + */ + private def classifyElementProps( + path: SchemaPath, + plan: JoinPlan, + node: Boolean): Map[Int, ElementProps] = { + val nodeVarIndex: Map[String, Int] = path.nodes.zipWithIndex.collect { + case (n, i) if n.variable.isDefined => n.variable.get -> i + }.toMap + val edgeVarIndex: Map[String, Int] = path.steps.zipWithIndex.collect { + case (s, i) if s.variable.isDefined => s.variable.get -> i + }.toMap + val varIndex = if (node) nodeVarIndex else edgeVarIndex + + // Per-element scan-filter properties (these are applied at the scan; they do NOT by themselves + // earn a carry -- they are consumed before the shuffle). + val scanFilterExprs: Seq[Expression] = + if (node) path.nodes.flatMap(_.scanFilter) else Seq.empty + val scanFilterProps: Map[Int, Set[String]] = collectProps(scanFilterExprs, varIndex) + + // Join/post-predicate properties: these DO earn a carry (they ride to the predicate's point). + val joinPostProps: Map[Int, Set[String]] = + collectProps(plan.joinPredicates ++ plan.postFilters, varIndex) + + // RETURN properties (only for Items projections). + val returnProps: Map[Int, Set[String]] = plan.projection match { + case Projection.Items(items) => collectProps(items.map(_.expression), varIndex) + case _ => Map.empty + } + + val acc = + scala.collection.mutable.Map.empty[Int, ElementProps].withDefaultValue(ElementProps.Empty) + + // For edges we should actually carry all the returnProps + varIndex.values.foreach { idx => + val sf = scanFilterProps.getOrElse(idx, Set.empty) + val jp = joinPostProps.getOrElse(idx, Set.empty) + val rp = returnProps.getOrElse(idx, Set.empty) + val carry = if (node) jp ++ rp.intersect(sf) else jp ++ rp + // there is no join-back for edges and all the rp are already carried along the join-path + val outputOnly = if (node) rp -- carry else Set.empty[String] + acc(idx) = ElementProps(carry, outputOnly) + } + acc.toMap + } + + /** Gather (elementIndex -> propertyNames) from the PropertyAccess nodes in `exprs`. */ + private def collectProps( + exprs: Seq[Expression], + varIndex: Map[String, Int]): Map[Int, Set[String]] = { + val acc = scala.collection.mutable.Map.empty[Int, Set[String]].withDefaultValue(Set.empty) + exprs.foreach { e => + propertyAccesses(e).foreach { case (v, p) => + varIndex.get(v).foreach { i => acc(i) = acc(i) + p } + } + } + acc.toMap + } + + /** Flatten all `(variable, property)` accesses appearing anywhere in `expr`. */ + private def propertyAccesses(expr: Expression): Seq[(String, String)] = expr match { + case PropertyAccess(v, p) => Seq((v, p)) + case Comparison(l, _, r) => propertyAccesses(l) ++ propertyAccesses(r) + case Arithmetic(l, _, r) => propertyAccesses(l) ++ propertyAccesses(r) + case Not(e) => propertyAccesses(e) + case And(l, r) => propertyAccesses(l) ++ propertyAccesses(r) + case Or(l, r) => propertyAccesses(l) ++ propertyAccesses(r) + case FunctionCall(_, args) => args.flatMap(propertyAccesses) + case _ => Seq.empty + } +} + +// ----------------------------------------------------------------------------- +// Prefix environment: maps each element to a unique column prefix so that joins +// never collide on the standardized column names (id/property_group/src/dst/weight). +// ----------------------------------------------------------------------------- + +private[propertygraph] final class PrefixEnv private (path: SchemaPath, val raw: Boolean) { + + /** Prefix for node `i`: the variable if named, else `node`. Empty when `raw`. */ + def nodePrefix(i: Int): String = + if (raw) "" else path.nodes(i).variable.getOrElse(s"node$i") + + /** Prefix for edge `i`: the variable if named, else `edge`. Empty when `raw`. */ + def edgePrefix(i: Int): String = + if (raw) "" else path.steps(i).variable.getOrElse(s"edge$i") + + /** Fully-qualified column name for a node's output column (e.g. the `id`). */ + def nodeCol(i: Int, colName: String): String = join(nodePrefix(i), colName) + + /** Fully-qualified column name for an edge's output column (`src`/`dst`/`weight`). */ + def edgeCol(i: Int, colName: String): String = join(edgePrefix(i), colName) + + /** + * Join a prefix and a column name: `colName` when the prefix is empty, else `prefix_colName`. + */ + def join(prefix: String, colName: String): String = + if (prefix.isEmpty) colName else s"${prefix}_$colName" + + /** The prefix string for a variable, if that variable names a node or edge in this path. */ + def prefixFor(variable: String): Option[String] = { + if (raw) Some("") + else + path.nodes.zipWithIndex + .find(_._1.variable.contains(variable)) + .map { case (_, i) => nodePrefix(i) } + .orElse(path.steps.zipWithIndex.find(_._1.variable.contains(variable)).map { + case (_, i) => + edgePrefix(i) + }) + } +} + +private[propertygraph] object PrefixEnv { + + /** A real path environment. */ + def apply(path: SchemaPath): PrefixEnv = new PrefixEnv(path, raw = false) + + /** + * A raw (empty-prefix) environment: every variable resolves to the empty prefix, so lowered + * columns reference raw group column names. Used for scan-local filters, which are applied by + * `getData` against the raw group data before aliasing. + */ + val raw: PrefixEnv = new PrefixEnv(null, raw = true) +} + +// ----------------------------------------------------------------------------- +// Expression -> Column lowering. +// ----------------------------------------------------------------------------- + +private[propertygraph] object ExpressionLowering { + + /** + * Lower a GQL AST [[Expression]] to a Spark [[Column]] in the context of `env` (variable -> + * prefix mapping). + * + * - `Variable(v)` -> `col("_id")` (a bare variable reference resolves to the + * element's id column); + * - `PropertyAccess(v, p)` -> `col("_

")`; + * - `Literal(value)` -> a typed `lit` (Long/Double/Boolean/String/null); + * - comparison/arithmetic/boolean combinators recurse and map to the corresponding Spark + * operators. + */ + def lower(expr: Expression, env: PrefixEnv): Column = expr match { + case Literal(value) => litLiteral(value) + case Variable(name) => + val prefix = env + .prefixFor(name) + .getOrElse( + throw new IllegalArgumentException( + s"Variable '$name' is not bound to any element of the matched path")) + col(env.join(prefix, GraphFrame.ID)) + case PropertyAccess(variable, property) => + val prefix = env + .prefixFor(variable) + .getOrElse( + throw new IllegalArgumentException( + s"Variable '$variable' is not bound to any element of the matched path")) + col(env.join(prefix, property)) + case Comparison(left, op, right) => compOp(lower(left, env), op, lower(right, env)) + case Arithmetic(left, op, right) => arithOp(lower(left, env), op, lower(right, env)) + case Not(e) => !lower(e, env) + case And(left, right) => lower(left, env) && lower(right, env) + case Or(left, right) => lower(left, env) || lower(right, env) + case FunctionCall(name, args) => + FunctionRegistry.lower(name, args, args.map(lower(_, env))) + } + + private def compOp(l: Column, op: CompOp, r: Column): Column = op match { + case Eq => l === r + case Neq => l =!= r + case Lt => l < r + case Lte => l <= r + case Gt => l > r + case Gte => l >= r + } + + private def arithOp(l: Column, op: ArithOp, r: Column): Column = op match { + case Plus => l + r + case Minus => l - r + case Mult => l * r + case Div => l / r // NB: Spark `/` is floating-point division (5/2 = 2.5) + case Mod => l % r // NB: result follows the dividend's sign + } + + /** Narrow `Literal.value: Any` to the appropriate Spark-typed literal. */ + private def litLiteral(value: Any): Column = value match { + case null => lit(null) + case v: java.lang.Boolean => lit(v.booleanValue()) + case v: java.lang.Long => lit(v.longValue()) + case v: java.lang.Integer => lit(v.intValue()) + case v: java.lang.Double => lit(v.doubleValue()) + case v: java.lang.Float => lit(v.floatValue()) + case v: Long => lit(v) + case v: Int => lit(v) + case v: Double => lit(v) + case v: Float => lit(v) + case v: String => lit(v) + case other => + throw new IllegalArgumentException( + s"Unsupported literal value of type ${other.getClass.getName}: $other") + } +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/QueryIr.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/QueryIr.scala new file mode 100644 index 000000000..c88a5e35d --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/QueryIr.scala @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +/** + * The output of resolution (`Resolver`). This is the logical, schema-resolved representation of a + * GQL query, before any join ordering / statistics / execution. + * + * @param paths + * 1..N concrete `SchemaPath`s fanned out from untyped/ambiguous pattern elements. Empty when + * the pattern is disconnected in the schema graph (no Spark execution will be needed + * downstream): fast-fail. + * @param joinPredicates + * WHERE conjuncts that span exactly two adjacent node variables; applied as join conditions. + * @param postFilters + * WHERE conjuncts that span 3+ variables, non-adjacent variables, or any edge variable; applied + * after the join tree. (Scan-local predicates live on each `PathNode`, not here.) + * @param projection + * the `RETURN` shape, or `Projection.Default` when `RETURN` is omitted. + */ +private[propertygraph] final case class ResolvedQuery( + paths: Seq[SchemaPath], + joinPredicates: Seq[Expression], + postFilters: Seq[Expression], + projection: Projection) + +/** + * Encodes `RETURN *` vs explicit items vs the omitted default. See design §5.3. + * + * `Default` projects the first and last *named* node IDs of each path (anonymous `()` nodes are + * not surfaced), matching the fixed output schema in the proposal §6. + */ +private[propertygraph] sealed trait Projection + +private[propertygraph] object Projection { + case object Default extends Projection // RETURN omitted + case object Star extends Projection // RETURN * + final case class Items(items: Seq[ReturnItem]) extends Projection // RETURN expr [AS alias], ... +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/Resolver.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/Resolver.scala new file mode 100644 index 000000000..1c91d43b4 --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/Resolver.scala @@ -0,0 +1,341 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +import org.graphframes.InvalidPropertyGroupException +import org.graphframes.propertygraph.QueryOptions + +/** + * Resolution: turns a `MatchStatement` AST plus a `SchemaGraphSnapshot` into a `ResolvedQuery`. + * Think about it as about Catalyst' analysis phase: we take a raw path-pattern and try to match + * it against the known LPG schema to determine all the possible join-chains. That is exactly why + * we need the GraphSchema and that is the biggest difference from the existing Motifs Finding + * API. + * + * As well we fail-fast in the case of syntax error in the query. + * + * Steps: + * 1. Validate every typed label against the schema (unknown label => + * `InvalidPropertyGroupException`). + * 2. Enumerate concrete `SchemaPath`s by a bounded DFS over `outgoing`/`incoming`, fanning out + * over untyped nodes/edges. Disconnected patterns yield no paths (empty, not an error). + * 3. Classify WHERE conjuncts into scan-local (attached to `PathNode`), join (two adjacent node + * vars), or post-join (everything else). + * 4. Map the RETURN clause to a `Projection`. + */ +private[propertygraph] object Resolver { + + def resolve( + ast: MatchStatement, + schema: SchemaGraphSnapshot, + options: QueryOptions): ResolvedQuery = { + val nodes = ast.pattern.elements.collect { case n: NodePattern => n } + val edges = ast.pattern.elements.collect { case e: EdgePattern => e } + // The grammar guarantees nodes.length == edges.length + 1; defend in depth. + require( + nodes.length == edges.length + 1, + s"GQL pattern must alternate node/edge/node; got ${nodes.length} nodes, ${edges.length} edges") + + validateLabels(nodes, edges, schema) + + val paths = enumeratePaths(nodes, edges, schema, options) + + val (joinPredicates, postFilters, nodeScanFilters, edgeScanFilters) = + classifyWhere(ast.where, nodes, edges) + + // Attach scan-local predicates to the matching PathNode(s) in every enumerated path. + val pathsWithFilters = paths + .map(attachNodeScanFilters(_, nodeScanFilters)) + .map(attachEdgeScanFilters(_, edgeScanFilters)) + + val projection = ast.returnClause match { + case Some(ReturnStar) => Projection.Star + case Some(ReturnItems(items)) => Projection.Items(items) + case None => Projection.Default + } + + ResolvedQuery(pathsWithFilters, joinPredicates, postFilters, projection) + } + + // --------------------------------------------------------------------- + // Step 1: label validation. + // --------------------------------------------------------------------- + + private def validateLabels( + nodes: Seq[NodePattern], + edges: Seq[EdgePattern], + schema: SchemaGraphSnapshot): Unit = { + val edgeGroupNames = schema.edges.map(_.edgeGroupName).toSet + nodes.foreach { n => + n.label.foreach { label => + if (!schema.vertexGroupNames.exists(_.equalsIgnoreCase(label))) { + throw new InvalidPropertyGroupException( + s"Unknown vertex label '$label'; known vertex groups: " + + schema.vertexGroupNames.toVector.sorted.mkString(", ")) + } + } + } + edges.foreach { e => + e.label.foreach { label => + if (!edgeGroupNames.exists(_.equalsIgnoreCase(label))) { + throw new InvalidPropertyGroupException( + s"Unknown edge label '$label'; known edge groups: " + + edgeGroupNames.toVector.sorted.mkString(", ")) + } + } + } + } + + // --------------------------------------------------------------------- + // Step 2: schema-graph path enumeration (bounded DFS). + // --------------------------------------------------------------------- + + // A note about the `enumerataPaths`: + // It is very naive and on the case of thousands of groups + // it may be quite a slow. At the same time, I cannot imagine the case + // when it became a bottleneck even with all it's scala wrappers, GC-pressure + // and multiple iterations over the same data. + private[propertygraph] def enumeratePaths( + nodes: Seq[NodePattern], + edges: Seq[EdgePattern], + schema: SchemaGraphSnapshot, + options: QueryOptions): Vector[SchemaPath] = { + // Start vertex-group candidates for node 0. + // Resolve the user-supplied label to its canonical-case name so that subsequent + // outgoing/incoming map lookups (keyed by original case) still hit. + val startGroups: Set[String] = + nodes.head.label + .map(label => schema.vertexGroupNames.find(_.equalsIgnoreCase(label)).toSet) + .getOrElse(schema.vertexGroupNames) + + val results = scala.collection.mutable.ListBuffer.empty[SchemaPath] + + def dfs( + nodeIndex: Int, + currentGroup: String, + accNodes: Vector[PathNode], + accSteps: Vector[PathStep]): Unit = { + val node = PathNode(currentGroup, nodes(nodeIndex).variable, scanFilter = Seq.empty) + val nodesSoFar = accNodes :+ node + + if (nodeIndex == edges.length) { + // Leaf: emit a complete path. + results += SchemaPath(nodesSoFar, accSteps) + return + } + + val edgePat = edges(nodeIndex) + val nextNodePat = nodes(nodeIndex + 1) + + edgePat.hopsRange match { + case None => + // single hop + getCandidates(edgePat, currentGroup, schema) + .foreach { case (edge, nextGroup, forward) => + // Filter by typed edge label, if any (case-insensitive). + val edgeLabelOk = edgePat.label.forall(_.equalsIgnoreCase(edge.edgeGroupName)) + // Filter by typed next-node label, if any (case-insensitive). + val nextLabelOk = nextNodePat.label.forall(_.equalsIgnoreCase(nextGroup)) + if (edgeLabelOk && nextLabelOk) { + val step = PathStep(edge, forward, edgePat.variable, scanFilter = Seq.empty) + dfs(nodeIndex + 1, nextGroup, nodesSoFar, accSteps :+ step) + } + } + // multi-hop + case Some((lo, hi)) => { + if ((hi > options.maxVarLength) || (hi - lo > options.maxVarLength) || (lo < 1) || (lo > hi)) { + throw new InvalidPropertyGroupException( + s"invalid variable length pattern $lo - $hi : it is required that lo >= 1, lo <= hi and lo - hi < ${options.maxVarLength}") + } else { + // for the 1-3 we should collect 1, 1-2, 1-2-3 + (lo to hi).foreach(lHops => + walkHops(currentGroup, lHops, edgePat, schema).foreach { + case (steps, intermediates, endGroup) => + if (nextNodePat.label.forall(_.equalsIgnoreCase(endGroup))) { + dfs(nodeIndex + 1, endGroup, nodesSoFar ++ intermediates, accSteps ++ steps) + } + }) + } + } + } + } + + startGroups.foreach(g => dfs(nodeIndex = 0, currentGroup = g, Vector.empty, Vector.empty)) + results.toVector + } + + // All L-hop walks from `fromGroup` over edges matching edgePat's label + direction. + // Returns (steps, intermediateNodes, endGroup). The final endpoint is NOT added as an + // intermediate — the caller's next dfs() appends the far pattern node. + def walkHops( + fromGroup: String, + hopsLeft: Int, + edgePat: EdgePattern, + schema: SchemaGraphSnapshot): Vector[(Vector[PathStep], Vector[PathNode], String)] = { + def go( + group: String, + left: Int, + steps: Vector[PathStep], + mids: Vector[PathNode]): Vector[(Vector[PathStep], Vector[PathNode], String)] = + if (left == 0) Vector((steps, mids, group)) + else + getCandidates(edgePat, group, schema).flatMap { case (edge, nextGroup, forward) => + if (edgePat.label.forall(_.equalsIgnoreCase(edge.edgeGroupName))) { + val step = PathStep(edge, forward, variable = None, scanFilter = Seq.empty) + val midsNext = + if (left == 1) mids // last hop: endpoint is the far node + else mids :+ PathNode(nextGroup, None, Seq.empty) // intermediate, anonymous + go(nextGroup, left - 1, steps :+ step, midsNext) + } else Vector.empty + } + go(fromGroup, hopsLeft, Vector.empty, Vector.empty) + } + + // Parse edge directions and handle deduplication logic + private def getCandidates( + edgePattern: EdgePattern, + currentGroup: String, + schema: SchemaGraphSnapshot): Vector[(SchemaEdge, String, Boolean)] = + edgePattern.direction match { + case LeftToRight => + // Pattern arrow agrees with src->dst: enumerate edges whose src is the current group. + schema.outgoing + .getOrElse(currentGroup, Vector.empty) + .map(e => (e, e.dstVertexGroupName, true)) + case RightToLeft => + // Pattern arrow opposes src->dst: enumerate edges whose dst is the current group, and the + // next node becomes the edge's src. + schema.incoming + .getOrElse(currentGroup, Vector.empty) + .map(e => (e, e.srcVertexGroupName, false)) + + case Undirected => { + // Undirected edge: combine both of the above + val backward = schema.incoming + .getOrElse(currentGroup, Vector.empty) + .map(e => (e, e.srcVertexGroupName, false)) + val forward = schema.outgoing + .getOrElse(currentGroup, Vector.empty) + .map(e => (e, e.dstVertexGroupName, true)) + + // if the edge is undirected we should deduplicate manually: + // for undirected edge Alice-Bob and Bob-Alice is the same edge + // for directed edge these two are different edges (so called "parallel") + // we should handle this. + (forward ++ backward.filterNot { case (edge, _, _) => + !edge.isDirected && edge.srcVertexGroupName.equalsIgnoreCase(edge.dstVertexGroupName) + }).toVector + } + } + + // --------------------------------------------------------------------- + // Step 3: WHERE classification. + // + // Returns (joinPredicates, postFilters, nodeScanFilters, edgeScanFilter) + // --------------------------------------------------------------------- + + private def classifyWhere( + whereOpt: Option[Expression], + nodes: Seq[NodePattern], + edges: Seq[EdgePattern]): ( + Seq[Expression], + Seq[Expression], + Map[String, Seq[Expression]], + Map[String, Seq[Expression]]) = { + // Variable -> node positions (0-based into `nodes`). The same variable may bind several + // positions (e.g. a triangle pattern `(a)-[..]->(b)-[..]->(a)`). + val nodeVarPositions: Map[String, Set[Int]] = + nodes.zipWithIndex + .flatMap { case (n, i) => n.variable.map(v => v -> i) } + .groupBy(_._1) + .map { case (v, pairs) => + v -> pairs.map(_._2).toSet + } + + val edgeVarNames: Set[String] = edges.flatMap(_.variable).toSet + + val join = scala.collection.mutable.ListBuffer.empty[Expression] + val post = scala.collection.mutable.ListBuffer.empty[Expression] + val nodeScan = scala.collection.mutable.Map.empty[String, Seq[Expression]] + val edgeScan = scala.collection.mutable.Map.empty[String, Seq[Expression]] + + val conjuncts = whereOpt.map(GqlAst.flattenAnd).getOrElse(Seq.empty) + conjuncts.foreach { conjunct => + val refs = GqlAst.referencedVariables(conjunct) + val nodeRefs = refs.intersect(nodeVarPositions.keySet) + val edgeRefs = refs.intersect(edgeVarNames) + if (edgeRefs.isEmpty && nodeRefs.size == 1) { + // Scan-local: a single node variable (possibly bound at several positions). + val v = nodeRefs.head + nodeScan(v) = nodeScan.getOrElse(v, Seq.empty) :+ conjunct + } else if (edgeRefs.isEmpty && nodeRefs.size == 2) { + val Seq(v1, v2) = nodeRefs.toSeq + if (areAdjacent(nodeVarPositions(v1), nodeVarPositions(v2))) { + join += conjunct + } else { + post += conjunct + } + } else if (edgeRefs.size == 1 && nodeRefs.isEmpty) { + // only one edge variable in WHERE: push it to the scan + val e = edgeRefs.head + edgeScan(e) = edgeScan.getOrElse(e, Seq.empty) :+ conjunct + } else { + // 3+ vars or a literal-only conjunct: evaluate after the join tree. + post += conjunct + } + } + + (join.toSeq, post.toSeq, nodeScan.toMap, edgeScan.toMap) + } + + /** + * Two node-position sets are adjacent if any pair of positions differs by exactly 1. Positions + * are indices into the nodes-only `Seq[NodePattern]` (edges already collected out), so a single + * edge hop connects nodes at indices `i` and `i+1`. + */ + private def areAdjacent(p1: Set[Int], p2: Set[Int]): Boolean = + p1.exists(a => p2.exists(b => Math.abs(a - b) == 1)) + + // Pushdown of vertex group "where" to the scan + private def attachNodeScanFilters( + path: SchemaPath, + nodeScanFilters: Map[String, Seq[Expression]]): SchemaPath = { + if (nodeScanFilters.isEmpty) path + else { + val newNodes = path.nodes.map { n => + val extra = n.variable.flatMap(nodeScanFilters.get).getOrElse(Seq.empty) + if (extra.isEmpty) n else n.copy(scanFilter = n.scanFilter ++ extra) + } + path.copy(nodes = newNodes) + } + } + + // Pushdown of edge group "where" to the scan + private def attachEdgeScanFilters( + path: SchemaPath, + edgeScanFilters: Map[String, Seq[Expression]]): SchemaPath = { + if (edgeScanFilters.isEmpty) path + else { + val newEdges = path.steps.map { p => + val extra = p.variable.flatMap(edgeScanFilters.get).getOrElse(Seq.empty) + if (extra.isEmpty) p else p.copy(scanFilter = p.scanFilter ++ extra) + } + path.copy(steps = newEdges) + } + } +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/internal/SchemaGraphSnapshot.scala b/core/src/main/scala/org/graphframes/propertygraph/internal/SchemaGraphSnapshot.scala new file mode 100644 index 000000000..2ed35a870 --- /dev/null +++ b/core/src/main/scala/org/graphframes/propertygraph/internal/SchemaGraphSnapshot.scala @@ -0,0 +1,165 @@ +package org.graphframes.propertygraph.internal + +import org.graphframes.propertygraph.PropertyGraphFrame + +/** + * A directed edge in the schema graph, representing the pure topological relationship between two + * vertex property groups via an edge property group. + * + * @param edgeGroupName + * name of the edge property group this schema edge corresponds to. + * @param srcVertexGroupName + * name of the source vertex property group. + * @param dstVertexGroupName + * name of the destination vertex property group. + * @param isDirected + * is edge directed + */ +private[propertygraph] final case class SchemaEdge( + edgeGroupName: String, + srcVertexGroupName: String, + dstVertexGroupName: String, + isDirected: Boolean) + +/** + * One resolved vertex slot in a schema path. + * + * @param vertexGroupName + * concrete vertex property group this slot was resolved to during schema-graph enumeration. + * @param variable + * the user-written pattern binding (e.g. `a`, `x`), or `None` for an anonymous `()` node. + * @param scanFilter + * scan-local WHERE predicates (AST) that reference only this node's variable; lowered to a + * Spark `Column` at execution time by the (deferred) executor. + */ +private[propertygraph] final case class PathNode( + vertexGroupName: String, + variable: Option[String], + scanFilter: Seq[Expression]) + +/** + * One resolved edge hop in a schema path. + * + * @param edge + * the resolved edge group (pure topology). + * @param traversedForward + * `true` when the pattern arrow agrees with the edge's `src -> dst` direction, `false` when the + * pattern arrow (`<-[e]-`) opposes it. Forward step joins `fromNode.id == edge.src` and + * `toNode.id == edge.dst`; backward step swaps src/dst. See design §7. + * @param variable + * the edge binding, if any (`-[e:KNOWS]->`). + */ +private[propertygraph] final case class PathStep( + edge: SchemaEdge, + traversedForward: Boolean, + variable: Option[String], + scanFilter: Seq[Expression]) + +/** + * A fully-resolved, concrete path through the schema graph: a linear chain of + * `PathNode`-`PathStep`-`PathNode`-... produced by enumerating the schema graph against a user + * pattern. Untyped pattern elements fan out into multiple `SchemaPath`s (one per candidate edge + * group); a pattern that is disconnected in the schema graph yields no paths. + */ +private[propertygraph] final case class SchemaPath( + nodes: Vector[PathNode], + steps: Vector[PathStep]) { + require(nodes.size == steps.size + 1) + def length: Int = steps.length + + override def toString: String = { + val sb = new StringBuilder("SchemaPath(") + for (i <- nodes.indices) { + if (i > 0) { + val step = steps(i - 1) + val arrow = if (step.traversedForward) "->" else "<-" + val edgeLabel = step.variable match { + case Some(v) => s"[$v:${step.edge.edgeGroupName}]" + case None => s"[${step.edge.edgeGroupName}]" + } + sb.append(s"$arrow$edgeLabel$arrow") + } + val node = nodes(i) + val nodeLabel = node.variable match { + case Some(v) => s"($v:${node.vertexGroupName})" + case None => s"(${node.vertexGroupName})" + } + sb.append(nodeLabel) + } + sb.append(")").toString() + } +} + +private[propertygraph] final case class SchemaGraphSnapshot( + vertexGroupNames: Set[String], + edges: Vector[SchemaEdge]) { + lazy val outgoing: Map[String, Vector[SchemaEdge]] = + edges.groupBy(_.srcVertexGroupName) + + lazy val incoming: Map[String, Vector[SchemaEdge]] = + edges.groupBy(_.dstVertexGroupName) +} + +private[propertygraph] object SchemaGraphSnapshot { + + def fromPropertyGraphFrame(pgf: PropertyGraphFrame): SchemaGraphSnapshot = { + val vertexNames = + pgf.vertexPropertyGroups.map(_.name).toSet + + val edges = + pgf.edgesPropertyGroups.map { eg => + SchemaEdge( + edgeGroupName = eg.name, + srcVertexGroupName = eg.srcPropertyGroup.name, + dstVertexGroupName = eg.dstPropertyGroup.name, + isDirected = eg.isDirected) + }.toVector + + SchemaGraphSnapshot(vertexNames, edges) + } + + def toDOT(snapshot: SchemaGraphSnapshot): String = { + def q(value: String): String = { + val escaped = value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + + val quote = "\"" // hack + s"$quote$escaped$quote" + } + + val sortedVertices = snapshot.vertexGroupNames.toVector.sorted + val sortedEdges = + snapshot.edges.sortBy(e => (e.srcVertexGroupName, e.dstVertexGroupName, e.edgeGroupName)) + + val vertexLines = sortedVertices.map(v => s" ${q(v)};") + + val edgeLines = sortedEdges.map { e => + s" ${q(e.srcVertexGroupName)} -> ${q(e.dstVertexGroupName)} [label=${q(e.edgeGroupName)}];" + } + + (Vector("digraph SchemaGraph {") ++ vertexLines ++ edgeLines ++ Vector("}")) + .mkString("\n") + } + + def toString(snapshot: SchemaGraphSnapshot): String = { + val sortedVertices = snapshot.vertexGroupNames.toVector.sorted + val sortedEdges = + snapshot.edges.sortBy(e => (e.srcVertexGroupName, e.dstVertexGroupName, e.edgeGroupName)) + + val vertexLines = + if (sortedVertices.isEmpty) Vector(" (none)") + else sortedVertices.map(v => s" - $v") + + val edgeLines = + if (sortedEdges.isEmpty) Vector(" (none)") + else + sortedEdges.map(e => + s" - ${e.edgeGroupName}: ${e.srcVertexGroupName} -> ${e.dstVertexGroupName}") + + (Vector("Property graph schema:", s"Vertex property groups (${sortedVertices.size}):") ++ + vertexLines ++ + Vector(s"Edge property groups (${sortedEdges.size}):") ++ + edgeLines).mkString("\n") + } +} diff --git a/core/src/main/scala/org/graphframes/propertygraph/property/EdgePropertyGroup.scala b/core/src/main/scala/org/graphframes/propertygraph/property/EdgePropertyGroup.scala index 4be1b338d..8410ba574 100644 --- a/core/src/main/scala/org/graphframes/propertygraph/property/EdgePropertyGroup.scala +++ b/core/src/main/scala/org/graphframes/propertygraph/property/EdgePropertyGroup.scala @@ -94,22 +94,33 @@ case class EdgePropertyGroup( col(dstColumnName).cast(StringType) } - override protected[graphframes] def getData(filter: Column): DataFrame = { + override private[propertygraph] def getData( + filter: Column, + requestedProperties: Seq[String]): DataFrame = { val filteredData = data.filter(filter) - val baseEdges = filteredData.select( + // Unknown requested property names are dropped silently; the query engine validates first. + val availableProperties = requestedProperties.filter(data.columns.contains) + + val baseCols = Seq( hashSrcEdge.alias(GraphFrame.SRC), hashDstEdge.alias(GraphFrame.DST), col(weightColumnName).alias(GraphFrame.WEIGHT)) + val baseEdges = filteredData.select(baseCols ++ availableProperties.map(col): _*) + if (isDirected) { baseEdges } else { + // For undirected edges, surface both orientations. The extra property columns are copied + // verbatim into both halves of the union (they describe the same row, just reoriented). + val propertyCols = availableProperties.map(c => col(c).alias(c)) baseEdges.union( baseEdges.select( - col(GraphFrame.DST).as(GraphFrame.SRC), - col(GraphFrame.SRC).as(GraphFrame.DST), - col(GraphFrame.WEIGHT).alias(GraphFrame.WEIGHT))) + (col(GraphFrame.DST).as(GraphFrame.SRC) + +: col(GraphFrame.SRC).as(GraphFrame.DST) + +: col(GraphFrame.WEIGHT).alias(GraphFrame.WEIGHT) + +: propertyCols): _*)) } } } diff --git a/core/src/main/scala/org/graphframes/propertygraph/property/PropertyGroup.scala b/core/src/main/scala/org/graphframes/propertygraph/property/PropertyGroup.scala index fd84e1c45..cec1b8278 100644 --- a/core/src/main/scala/org/graphframes/propertygraph/property/PropertyGroup.scala +++ b/core/src/main/scala/org/graphframes/propertygraph/property/PropertyGroup.scala @@ -15,16 +15,39 @@ trait PropertyGroup { * @return * A DataFrame containing the raw data. */ - protected[graphframes] def getData(): DataFrame = getData(lit(true)) + private[propertygraph] def getData(): DataFrame = getData(lit(true)) /** - * Returns a filtered view of the data for the property group, with an optional mask applied to - * IDs. + * Returns a filtered view of the data for the property group without requesting any extra + * property columns (only id/standardized columns are projected). Equivalent to + * `getData(filter, Seq.empty)`. * * @param filter * A condition (Column) used to filter the data. * @return * A DataFrame containing the filtered and optionally transformed data. */ - protected[graphframes] def getData(filter: Column): DataFrame + private[propertygraph] def getData(filter: Column): DataFrame = getData(filter, Seq.empty) + + /** + * Returns a filtered view of the data for the property group, with an optional mask applied to + * IDs, and additionally carrying the named property columns through to the output. + * + * The extra `requestedProperties` are useful for query engines that need to surface specific + * properties (e.g. a `RETURN a.age` projection) without re-reading the raw `data`. They are + * passed through unmodified — they are not join keys and are never masked. When + * `requestedProperties` is empty, the output is identical to `getData(filter)`. + * + * @param filter + * A condition (Column) used to filter the data. + * @param requestedProperties + * Names of additional property columns to carry through to the output. + * @return + * A DataFrame containing the filtered, optionally transformed data plus requested properties. + */ + private[propertygraph] def getData(filter: Column, requestedProperties: Seq[String]): DataFrame + + /** Convenience overload of [[getData(filter, requestedProperties)* ]] with no filter. */ + private[propertygraph] def getData(requestedProperties: Seq[String]): DataFrame = + getData(lit(true), requestedProperties) } diff --git a/core/src/main/scala/org/graphframes/propertygraph/property/VertexPropertyGroup.scala b/core/src/main/scala/org/graphframes/propertygraph/property/VertexPropertyGroup.scala index caff7cbfd..422081e34 100644 --- a/core/src/main/scala/org/graphframes/propertygraph/property/VertexPropertyGroup.scala +++ b/core/src/main/scala/org/graphframes/propertygraph/property/VertexPropertyGroup.scala @@ -57,23 +57,33 @@ case class VertexPropertyGroup( this } - private[graphframes] def internalIdMapping: DataFrame = data + private[propertygraph] def internalIdMapping: DataFrame = data .select(col(primaryKeyColumn).alias(PropertyGraphFrame.EXTERNAL_ID)) .withColumn( GraphFrame.ID, concat(lit(name), sha2(col(PropertyGraphFrame.EXTERNAL_ID).cast(StringType), 256))) - override protected[graphframes] def getData(filter: Column): DataFrame = { - val filteredData = data - .filter(filter) - val withId = if (applyMaskOnId) { - filteredData.select( - concat(lit(name), sha2(col(primaryKeyColumn).cast(StringType), 256)).alias(GraphFrame.ID)) + override private[propertygraph] def getData( + filter: Column, + requestedProperties: Seq[String]): DataFrame = { + val filteredData = data.filter(filter) + + // The masked/raw id column is always emitted first, regardless of requested properties. + val idCol = if (applyMaskOnId) { + concat(lit(name), sha2(col(primaryKeyColumn).cast(StringType), 256)) } else { - filteredData.select(col(primaryKeyColumn).cast(StringType).alias(GraphFrame.ID)) + col(primaryKeyColumn).cast(StringType) } - withId.select(col(GraphFrame.ID), lit(name).alias(PropertyGraphFrame.PROPERTY_GROUP_COL_NAME)) + // Requested properties are carried through unmodified (they are not join keys, never masked). + // Unknown requested property names are dropped silently -- the caller (the query engine) is + // expected to validate them against `data.columns` before requesting. + val availableProperties = requestedProperties.filter(data.columns.contains) + + val baseCols = + Seq(idCol.alias(GraphFrame.ID), lit(name).alias(PropertyGraphFrame.PROPERTY_GROUP_COL_NAME)) + + filteredData.select(baseCols ++ availableProperties.map(col): _*) } } diff --git a/core/src/test/scala/org/graphframes/propertygraph/PropertyGraphFrameCaseInsensitiveSuite.scala b/core/src/test/scala/org/graphframes/propertygraph/PropertyGraphFrameCaseInsensitiveSuite.scala new file mode 100644 index 000000000..42bc314e3 --- /dev/null +++ b/core/src/test/scala/org/graphframes/propertygraph/PropertyGraphFrameCaseInsensitiveSuite.scala @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph + +import org.apache.spark.sql.functions.lit +import org.graphframes.GraphFrameTestSparkContext +import org.graphframes.SparkFunSuite +import org.graphframes.propertygraph.property.EdgePropertyGroup +import org.graphframes.propertygraph.property.VertexPropertyGroup + +import java.security.MessageDigest + +/** + * Regression tests that lock in case-insensitive label matching in the public property-graph + * Scala API: [[PropertyGraphFrame.toGraphFrame]] and [[PropertyGraphFrame.projectionBy]]. Group + * names passed with different casing than they were registered with must still resolve. + */ +class PropertyGraphFrameCaseInsensitiveSuite + extends SparkFunSuite + with GraphFrameTestSparkContext { + + import sqlImplicits._ + + private var graph: PropertyGraphFrame = _ + + // Groups are registered with distinctive casing; queries below deliberately use other cases. + private val peopleName = "People" + private val moviesName = "Movies" + private val likesName = "Likes" + + override def beforeAll(): Unit = { + super.beforeAll() + val peopleData = Seq((1L, "Alice"), (2L, "Bob")).toDF("id", "name") + val peopleGroup = VertexPropertyGroup(peopleName, peopleData, "id") + + val moviesData = Seq((10L, "Matrix"), (20L, "Inception")).toDF("id", "title") + val moviesGroup = VertexPropertyGroup(moviesName, moviesData, "id") + + val likesData = Seq((1L, 10L), (2L, 20L)).toDF("src", "dst") + val likesGroup = EdgePropertyGroup( + likesName, + likesData, + peopleGroup, + moviesGroup, + isDirected = true, + "src", + "dst", + lit(1.0)) + + graph = PropertyGraphFrame(Seq(peopleGroup, moviesGroup), Seq(likesGroup)) + } + + // Mirrors the internal ID masking so tests can compare against hashed IDs. + private def sha256Hash(id: Long, groupName: String): String = { + val md = MessageDigest.getInstance("SHA-256") + val hash = md.digest(id.toString.getBytes("UTF-8")).map("%02x".format(_)).mkString + s"$groupName$hash" + } + + // ----- toGraphFrame ------------------------------------------------------- + + test("toGraphFrame resolves lowercase group names") { + val gf = graph.toGraphFrame( + Seq("people"), + Seq("likes"), + Map("likes" -> lit(true)), + Map("people" -> lit(true))) + assert(gf.vertices.count() === 2) + assert(gf.edges.count() === 2) + } + + test("toGraphFrame resolves uppercase group names") { + val gf = graph.toGraphFrame( + Seq("PEOPLE", "MOVIES"), + Seq("LIKES"), + Map("LIKES" -> lit(true)), + Map("PEOPLE" -> lit(true), "MOVIES" -> lit(true))) + assert(gf.vertices.count() === 4) + assert(gf.edges.count() === 2) + } + + test("toGraphFrame resolves mixed-case group names") { + val gf = graph.toGraphFrame( + Seq("pEoPlE"), + Seq("lIkEs"), + Map("lIkEs" -> lit(true)), + Map("pEoPlE" -> lit(true))) + assert(gf.vertices.count() === 2) + assert(gf.edges.count() === 2) + } + + test("toGraphFrame preserves canonical IDs despite query casing") { + val gf = graph.toGraphFrame( + Seq("people"), + Seq("likes"), + Map("likes" -> lit(true)), + Map("people" -> lit(true))) + val expectedVertices = Set(sha256Hash(1L, peopleName), sha256Hash(2L, peopleName)) + val actualVertices = gf.vertices.collect().map(_.getString(0)).toSet + assert(actualVertices === expectedVertices) + } + + test("toGraphFrame rejects unknown vertex group") { + intercept[IllegalArgumentException] { + graph.toGraphFrame( + Seq("Nonexistent"), + Seq("likes"), + Map("likes" -> lit(true)), + Map("Nonexistent" -> lit(true))) + } + } + + test("toGraphFrame rejects unknown edge group") { + intercept[IllegalArgumentException] { + graph.toGraphFrame( + Seq("people"), + Seq("Hates"), + Map("Hates" -> lit(true)), + Map("people" -> lit(true))) + } + } + + // ----- projectionBy ------------------------------------------------------- + + test("projectionBy resolves lowercase group names") { + val projected = graph.projectionBy( + leftBiGraphPart = "people", // registered as "People" + rightBiGraphPart = "movies", // registered as "Movies" + edgeGroup = "likes" + ) // registered as "Likes" + // Projection drops the "through" group (movies); one projected group remains. + assert(projected.vertexPropertyGroups.map(_.name).toSet === Set(peopleName)) + // The projected edge group name echoes the query casing by design. + assert(projected.edgesPropertyGroups.map(_.name).toSet === Set("projected_likes")) + } + + test("projectionBy resolves uppercase group names") { + val projected = graph.projectionBy( + leftBiGraphPart = "PEOPLE", + rightBiGraphPart = "MOVIES", + edgeGroup = "LIKES") + assert(projected.vertexPropertyGroups.map(_.name).toSet === Set(peopleName)) + assert(projected.edgesPropertyGroups.map(_.name).toSet === Set("projected_LIKES")) + } + + test("projectionBy rejects unknown edge group") { + intercept[NoSuchElementException] { + graph.projectionBy( + leftBiGraphPart = "people", + rightBiGraphPart = "movies", + edgeGroup = "Hates") + } + } +} diff --git a/core/src/test/scala/org/graphframes/propertygraph/PropertyGraphFrameQuerySuite.scala b/core/src/test/scala/org/graphframes/propertygraph/PropertyGraphFrameQuerySuite.scala new file mode 100644 index 000000000..340c389db --- /dev/null +++ b/core/src/test/scala/org/graphframes/propertygraph/PropertyGraphFrameQuerySuite.scala @@ -0,0 +1,670 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph + +import org.apache.spark.sql.DataFrame +import org.apache.spark.sql.Row +import org.apache.spark.sql.functions.lit +import org.graphframes.GraphFrameTestSparkContext +import org.graphframes.InvalidParseException +import org.graphframes.InvalidPropertyGroupException +import org.graphframes.SparkFunSuite +import org.graphframes.propertygraph.property.EdgePropertyGroup +import org.graphframes.propertygraph.property.VertexPropertyGroup + +import java.security.MessageDigest + +/** + * End-to-end tests for the public [[PropertyGraphFrame.query]] / [[PropertyGraphFrame.explain]] + * API: parse -> resolve -> plan -> execute, exercising the full pipeline through the public + * surface. + */ +class PropertyGraphFrameQuerySuite extends SparkFunSuite with GraphFrameTestSparkContext { + + import sqlImplicits._ + + private var pgf: PropertyGraphFrame = _ + + override def beforeAll(): Unit = { + super.beforeAll() + val persons = Seq((1L, "Alice", 30), (2L, "Bob", 40)).toDF("id", "name", "age") + val companies = Seq((10L, "Acme")).toDF("id", "name") + val personGroup = VertexPropertyGroup("Person", persons, "id") + val companyGroup = VertexPropertyGroup("Company", companies, "id") + + val knows = Seq((1L, 2L), (2L, 1L)).toDF("src", "dst") + val worksAt = Seq((1L, 10L)).toDF("src", "dst") + val knowsGroup = EdgePropertyGroup( + "KNOWS", + knows, + personGroup, + personGroup, + isDirected = true, + "src", + "dst", + lit(1.0)) + val worksAtGroup = EdgePropertyGroup( + "WORKS_AT", + worksAt, + personGroup, + companyGroup, + isDirected = true, + "src", + "dst", + lit(1.0)) + + pgf = PropertyGraphFrame(Seq(personGroup, companyGroup), Seq(knowsGroup, worksAtGroup)) + } + + // ------------------------------------------------------------------------- + // Path-comparison helpers. + // + // The query output uses masked ids (`concat(groupName, sha2(id, 256))`) for every id column, + // and a `path` array of (edge_property_group, node_id, node_property_group) structs whose final + // entry carries only the edge group (the end node lives in `end_id`). These helpers let tests + // express expectations in terms of *raw* ids + group names and assert equality of the full + // result set, not just the row count. + // ------------------------------------------------------------------------- + + /** Mask an external id the same way [[VertexPropertyGroup]] does internally. */ + private def maskedId(id: Any, groupName: String): String = { + val md = MessageDigest.getInstance("SHA-256") + val hash = md.digest(id.toString.getBytes("UTF-8")).map("%02x".format(_)).mkString + s"$groupName$hash" + } + + /** + * One entry of the `path` array. [[nodeId]]/[[nodeGroup]] are [[None]] for the trailing entry + * of a multi-hop path (where the end node is already captured by `end_id`), and for the single + * entry of a single-hop path. + */ + case class ExpectedHop( + edgeGroup: String, + nodeId: Option[Long] = None, + nodeGroup: Option[String] = None) + + object ExpectedHop { + + /** Intermediate hop carrying an intermediate node. */ + def mid(edgeGroup: String, nodeId: Long, nodeGroup: String): ExpectedHop = + ExpectedHop(edgeGroup, Some(nodeId), Some(nodeGroup)) + + /** Trailing hop: edge group only, no node (the end node lives in `end_id`). */ + def last(edgeGroup: String): ExpectedHop = ExpectedHop(edgeGroup, None, None) + } + + /** A full expected output row, expressed in terms of raw ids and group names. */ + case class ExpectedPath( + startId: Long, + startGroup: String, + endId: Long, + endGroup: String, + edgeGroup: String, + hops: Seq[ExpectedHop]) + + /** Normalized representation of an actual DataFrame row (masked ids inlined). */ + private case class ActualPath( + startId: String, + startGroup: String, + endId: String, + endGroup: String, + edgeGroup: String, + hops: Seq[(String, Option[String], Option[String])]) + + /** Extract an [[ActualPath]] from a result row. */ + private def rowToActual(row: Row): ActualPath = { + val pathArr = row + .get(row.fieldIndex("path")) + .asInstanceOf[scala.collection.Seq[Row]] + val hops = pathArr.map { h => + val eg = h.getAs[String](0) + val nodeId = if (h.isNullAt(1)) None else Some(h.getAs[String](1)) + val nodeGroup = if (h.isNullAt(2)) None else Some(h.getAs[String](2)) + (eg, nodeId, nodeGroup) + } + ActualPath( + startId = row.getAs[String]("start_id"), + startGroup = row.getAs[String]("start_property_group"), + endId = row.getAs[String]("end_id"), + endGroup = row.getAs[String]("end_property_group"), + edgeGroup = row.getAs[String]("edge_property_group"), + hops = hops.toSeq) + } + + /** Convert an [[ExpectedPath]] (raw ids) to the comparable [[ActualPath]] (masked ids). */ + private def expectedToActual(p: ExpectedPath): ActualPath = ActualPath( + startId = maskedId(p.startId, p.startGroup), + startGroup = p.startGroup, + endId = maskedId(p.endId, p.endGroup), + endGroup = p.endGroup, + edgeGroup = p.edgeGroup, + hops = p.hops.map { h => + (h.edgeGroup, h.nodeId.map(id => maskedId(id, h.nodeGroup.get)), h.nodeGroup) + }) + + /** + * Collect `df` and assert that its rows match exactly the given [[ExpectedPath]]s (order + * independent). Compares start/end ids (masked), property groups, the first edge group, and + * every hop of the `path` array — so it catches missing/extra paths, wrong edge labels, and + * wrong intermediate nodes, not just row count. + */ + private def comparePaths( + df: DataFrame, + expected: Seq[ExpectedPath]): org.scalatest.Assertion = { + val actual = df.collect().map(rowToActual).toSet + val expectedActuals = expected.map(expectedToActual).toSet + assert( + actual === expectedActuals, + s"""|Query result mismatch. + | expected (${expectedActuals.size}): + |${expectedActuals + .map(" " + _.productIterator.mkString(", ")) + .toVector + .sorted + .mkString("\n")} + | actual (${actual.size}): + |${actual.map(" " + _.productIterator.mkString(", ")).toVector.sorted.mkString("\n")} + |""".stripMargin) + } + + test("query returns rows over the fixed schema") { + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person)") + assert(df.count() === 2) + assert( + df.schema.fieldNames === + Seq( + "start_id", + "start_property_group", + "end_id", + "end_property_group", + "edge_property_group", + "path")) + } + + test("query with a scan-local filter prunes rows") { + // Only Bob(40) has age > 30; Bob KNOWS Alice. So exactly one row, starting at Bob. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > 30") + assert(df.count() === 1) + } + + test("query with RETURN projects the requested columns") { + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name AS who") + assert(df.schema.fieldNames.toSeq === Seq("who")) + assert(df.count() === 2) + } + + test("query on a disconnected pattern returns an empty DataFrame without throwing") { + val df = pgf.query("MATCH (a:Company)-[:KNOWS]->(b:Person)") + assert(df.count() === 0) + } + + test("explain(logical) returns a non-empty string describing the resolved plan") { + val s = pgf.explain("MATCH (a:Person)-[:KNOWS]->(b:Person)") + assert(s.contains("Logical plan")) + assert(s.contains("KNOWS")) + } + + test("explain(physical) returns a non-empty string describing the join plan") { + val s = pgf.explain("MATCH (a:Person)-[:KNOWS]->(b:Person)", ExplainMode.Physical) + assert(s.contains("Physical plan")) + assert(s.contains("join order")) + } + + test("explain(logical) is the default mode") { + val s = pgf.explain("MATCH (a:Person)-[:KNOWS]->(b:Person)", ExplainMode.Logical) + assert(s.contains("Logical plan")) + } + + test("query with bad syntax throws InvalidParseException") { + intercept[InvalidParseException] { + pgf.query("MATCH (a:Person OPTIONAL MATCH") + } + } + + test("query with an unknown label throws InvalidPropertyGroupException") { + intercept[InvalidPropertyGroupException] { + pgf.query("MATCH (a:Unicorn)-[:KNOWS]->(b:Person)") + } + } + + test("QueryOptions.maxSchemaPathLength must be positive") { + // `PropertyGraphFrame.resolve` runs `require(maxSchemaPathLength > 0)` *before* any per-path + // depth check, so a non-positive cap is rejected regardless of the query's actual hop count. + // The WORKS_AT pattern here resolves to a length-1 schema path (a single Person->Company + // hop), so this guards the positivity precondition, not per-path depth -- the real per-path + // depth guard is covered by the boundary test below and the explain-bypass test. + intercept[IllegalArgumentException] { + pgf.query( + "MATCH (a:Person)-[:WORKS_AT]->(c:Company)", + QueryOptions(maxSchemaPathLength = 0)) + } + } + + test("maxSchemaPathLength per-path guard rejects patterns deeper than the cap") { + // The 2-hop KNOWS->KNOWS chain resolves to a length-2 schema path. With the cap set to 1 the + // per-path `require(path.length <= maxSchemaPathLength)` in `resolve` fires; with the cap + // raised to 2 the same query succeeds and returns the two 2-hop cycles. + val gql = "MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)" + intercept[IllegalArgumentException] { + pgf.query(gql, QueryOptions(maxSchemaPathLength = 1)) + } + val df = pgf.query(gql, QueryOptions(maxSchemaPathLength = 2)) + comparePaths( + df, + Seq( + ExpectedPath( + startId = 1L, + startGroup = "Person", + endId = 1L, + endGroup = "Person", + edgeGroup = "KNOWS", + hops = Seq(ExpectedHop.mid("KNOWS", 2L, "Person"), ExpectedHop.last("KNOWS"))), + ExpectedPath( + startId = 2L, + startGroup = "Person", + endId = 2L, + endGroup = "Person", + edgeGroup = "KNOWS", + hops = Seq(ExpectedHop.mid("KNOWS", 1L, "Person"), ExpectedHop.last("KNOWS"))))) + } + + test("explain bypasses the maxSchemaPathLength per-path guard so users can inspect the plan") { + // A 2-hop (length-2) pattern capped at 1: query throws, explain renders the plan instead. + val gql = "MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)" + val opts = QueryOptions(maxSchemaPathLength = 1) + intercept[IllegalArgumentException] { + pgf.query(gql, opts) + } + val logical = pgf.explain(gql, ExplainMode.Logical, opts) + assert(logical.contains("Logical plan")) + val physical = pgf.explain(gql, ExplainMode.Physical, opts) + assert(physical.contains("Physical plan")) + } + + // ------------------------------------------------------------------------- + // Real result checks via comparePaths. + // + // Each test below pins down the *exact* set of matched paths (start/end ids, + // property groups, the first edge group, and every hop of the `path` array), + // not just the row count. Graph fixture (see beforeAll): + // + // vertices: Person(1,Alice,30) Person(2,Bob,40) Company(10,Acme) + // edges: KNOWS 1->2, 2->1 (directed, Person->Person) + // WORKS_AT 1->10 (directed, Person->Company) + // ------------------------------------------------------------------------- + + test("single-hop KNOWS returns exactly the two directed edges with a single-hop path array") { + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person)") + comparePaths( + df, + Seq( + ExpectedPath(1L, "Person", 2L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))), + ExpectedPath(2L, "Person", 1L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("single-hop KNOWS includes the first edge group on every row") { + // The fixed schema surfaces the *first* edge group in `edge_property_group`. For a single-hop + // query there is only one edge, so every row must carry KNOWS -- never null, never anything + // else. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val edgeGroups = df.collect().map(_.getAs[String]("edge_property_group")).toSet + assert(edgeGroups === Set("KNOWS")) + } + + test("backward arrow <-[:KNOWS]- swaps start and end ids") { + // knows: 1->2, 2->1. Writing the arrow backwards binds a=dst, b=src, so we get (2,1) and (1,2) + // -- the same set of vertex pairs but with start/end flipped relative to the forward query. + val df = pgf.query("MATCH (a:Person)<-[:KNOWS]-(b:Person)") + comparePaths( + df, + Seq( + ExpectedPath(2L, "Person", 1L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))), + ExpectedPath(1L, "Person", 2L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("cross-group WORKS_AT path connects Person to Company") { + val df = pgf.query("MATCH (a:Person)-[:WORKS_AT]->(c:Company)") + comparePaths( + df, + Seq( + ExpectedPath( + 1L, + "Person", + 10L, + "Company", + "WORKS_AT", + Seq(ExpectedHop.last("WORKS_AT"))))) + } + + test("edge-label isolation: KNOWS query never returns WORKS_AT paths") { + // Only the KNOWS rows may appear; the WORKS_AT 1->10 path must NOT leak in even though Person + // appears on both sides of both edge groups. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val rows = df.collect() + assert(rows.nonEmpty) + rows.foreach { r => + assert(r.getAs[String]("edge_property_group") === "KNOWS") + assert(r.getAs[String]("start_property_group") === "Person") + assert(r.getAs[String]("end_property_group") === "Person") + } + } + + test("edge-label isolation: WORKS_AT query never returns KNOWS paths") { + val df = pgf.query("MATCH (a:Person)-[:WORKS_AT]->(c:Company)") + val rows = df.collect() + assert(rows.nonEmpty) + rows.foreach { r => + assert(r.getAs[String]("edge_property_group") === "WORKS_AT") + assert(r.getAs[String]("end_property_group") === "Company") + } + } + + test("untyped edge -[]-> over Person->Person fans out to KNOWS only") { + // The schema has exactly one outgoing edge from Person back to Person (KNOWS); WORKS_AT points + // Person->Company. So an untyped Person->Person arrow must resolve to KNOWS and yield the same + // two rows as the typed KNOWS query -- nothing more. + val df = pgf.query("MATCH (a:Person)-[]->(b:Person)") + comparePaths( + df, + Seq( + ExpectedPath(1L, "Person", 2L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))), + ExpectedPath(2L, "Person", 1L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("untyped trailing node () is resolved by the schema but not surfaced in end_id") { + // MATCH (a:Person)-[:WORKS_AT]->() : the trailing () resolves to Company (the only dst of + // WORKS_AT in the schema), so the edge is found and the row is returned. BUT the fixed output + // schema surfaces only the first/last *named* node ids (see QueryIr Projection.Default), so + // with only `a` named, end_id falls back to the last named node -- here `a` itself. The edge + // group is still WORKS_AT, proving the schema-resolved hop was executed. + val df = pgf.query("MATCH (a:Person)-[:WORKS_AT]->()") + val rows = df.collect() + assert(rows.length === 1) + val r = rows.head + assert(r.getAs[String]("start_property_group") === "Person") + assert(r.getAs[String]("edge_property_group") === "WORKS_AT") + // Quirk: with no named end variable, end_id/end_property_group mirror the last named node (a). + assert(r.getAs[String]("end_id") === r.getAs[String]("start_id")) + assert(r.getAs[String]("end_property_group") === "Person") + } + + test("scan-local WHERE a.age > 30 leaves exactly the Bob->Alice KNOWS row") { + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > 30") + // Only Bob(40) passes the scan filter; Bob KNOWS Alice. Exactly one row. + comparePaths( + df, + Seq(ExpectedPath(2L, "Person", 1L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("join-level WHERE a.age > b.age filters the joined frame") { + // KNOWS: 1(30)->2(40), 2(40)->1(30). a.age > b.age: only 40 > 30, i.e. Bob->Alice. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > b.age") + comparePaths( + df, + Seq(ExpectedPath(2L, "Person", 1L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("RETURN a.name AS who projects the exact source-name values") { + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name AS who") + assert(df.schema.fieldNames.toSeq === Seq("who")) + // KNOWS: 1->2 (Alice), 2->1 (Bob). The projected `who` set is exactly {Alice, Bob}. + val names = df.collect().map(_.getString(0)).toSet + assert(names === Set("Alice", "Bob")) + } + + test("RETURN a.name, b.name projects both endpoint names in order") { + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name") + assert(df.schema.fieldNames.toSeq === Seq("name", "name")) + val pairs = df.collect().map(r => (r.getString(0), r.getString(1))).toSet + assert(pairs === Set(("Alice", "Bob"), ("Bob", "Alice"))) + } + + test("disconnected pattern yields an empty result set with the fixed schema") { + val df = pgf.query("MATCH (a:Company)-[:KNOWS]->(b:Person)") + assert(df.count() === 0) + assert( + df.schema.fieldNames === + Seq( + "start_id", + "start_property_group", + "end_id", + "end_property_group", + "edge_property_group", + "path")) + } + + test("disconnected edge label yields an empty result set") { + // There is no WORKS_AT edge touching another Person, so Person-[:WORKS_AT]->Person is empty. + val df = pgf.query("MATCH (a:Person)-[:WORKS_AT]->(b:Person)") + assert(df.count() === 0) + } + + test("multi-hop path array carries intermediate node ids and groups") { + // The 2-hop chain Person-[:WORKS_AT]->Company-[:LOCATED_IN]->City is not present in this small + // fixture, but a 2-hop KNOWS->KNOWS chain is. Here we instead use a 2-hop KNOWS->KNOWS query + // to verify the `path` array structure: two entries, the first with the intermediate Person + // node, the second carrying only the edge group. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)") + // knows: 1->2, 2->1. A 2-hop chain a-b-c requires b's KNOWS-out to land on c. The only valid + // 2-hop chains are 1->2->1 and 2->1->2. + comparePaths( + df, + Seq( + ExpectedPath( + startId = 1L, + startGroup = "Person", + endId = 1L, + endGroup = "Person", + edgeGroup = "KNOWS", + hops = Seq(ExpectedHop.mid("KNOWS", 2L, "Person"), ExpectedHop.last("KNOWS"))), + ExpectedPath( + startId = 2L, + startGroup = "Person", + endId = 2L, + endGroup = "Person", + edgeGroup = "KNOWS", + hops = Seq(ExpectedHop.mid("KNOWS", 1L, "Person"), ExpectedHop.last("KNOWS"))))) + } + + test("multi-hop path array length equals the number of steps") { + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)") + df.collect().foreach { row => + val pathArr = row + .get(row.fieldIndex("path")) + .asInstanceOf[scala.collection.Seq[_]] + assert(pathArr.length === 2, s"2-step path must produce a 2-entry array: $row") + } + } + + test("combined scan-local and join predicates both apply") { + // a.age > 30 keeps only Bob as `a` (scan-local). b.age > 30 on Alice(30) vs Bob(40)... Alice + // is 30 so b.age > 30 is false. Result: empty. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > 30 AND b.age > 30") + assert(df.count() === 0) + } + + // ------------------------------------------------------------------------- + // ExpressionLowering coverage. + // + // The tests above only exercise `>` (Comparison.Gt), `AND`, and integer literals. The tests + // below pin down the remaining branches of `ExpressionLowering.lower` end-to-end through the + // public query API: OR, NOT, string literals, the rest of the comparison operators (Neq, Lte, + // Gte), arithmetic (+/-) in WHERE and RETURN, scan-local filters on the non-start variable, and + // non-adjacent post-filters. Same fixture: + // Person(1,Alice,30) Person(2,Bob,40); KNOWS 1->2, 2->1. + // ------------------------------------------------------------------------- + + test("WHERE OR keeps only the row matching either disjunct") { + // a.age > 35 keeps only Bob(40); a.age < 28 keeps neither. OR -> only Bob remains as `a`, + // and Bob KNOWS Alice, so exactly the Bob->Alice row. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > 35 OR a.age < 28") + comparePaths( + df, + Seq(ExpectedPath(2L, "Person", 1L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("WHERE NOT negates a comparison predicate") { + // NOT (a.age > 35) keeps Alice(30) and excludes Bob(40). Alice KNOWS Bob -> Alice->Bob. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE NOT (a.age > 35)") + comparePaths( + df, + Seq(ExpectedPath(1L, "Person", 2L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("WHERE string equality filters on a string property") { + // Exercises Comparison(Eq) with a String Literal against a string column. a.name = 'Alice' + // keeps only Alice as `a`; Alice KNOWS Bob -> Alice->Bob. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.name = 'Alice'") + comparePaths( + df, + Seq(ExpectedPath(1L, "Person", 2L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("WHERE <> (not-equal) prunes the matching endpoint") { + // Comparison.Neq via `<>`. a.age <> 40 excludes Bob(40) and keeps Alice(30) -> Alice->Bob. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age <> 40") + comparePaths( + df, + Seq(ExpectedPath(1L, "Person", 2L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("WHERE <= keeps rows at or below the threshold") { + // Comparison.Lte. a.age <= 30 keeps only Alice(30) -> Alice->Bob. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age <= 30") + comparePaths( + df, + Seq(ExpectedPath(1L, "Person", 2L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("WHERE >= keeps rows at or above the threshold") { + // Comparison.Gte. a.age >= 40 keeps only Bob(40) -> Bob->Alice. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age >= 40") + comparePaths( + df, + Seq(ExpectedPath(2L, "Person", 1L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("arithmetic Minus in WHERE participates in the join filter") { + // Arithmetic(Minus) lowered inside a join-level predicate spanning adjacent a,b. a.age - 5: + // Alice->Bob: 30 - 5 = 25 > 40 ? no + // Bob->Alice: 40 - 5 = 35 > 30 ? yes -> only Bob->Alice. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age - 5 > b.age") + comparePaths( + df, + Seq(ExpectedPath(2L, "Person", 1L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("arithmetic Plus in RETURN projects a computed column") { + // Arithmetic(Plus) inside a RETURN item, projected under an explicit alias. Exercises the + // Projection.Items path with a non-trivial expression (not a bare Variable/PropertyAccess), + // so the alias must be taken from the AS clause. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.age + 1 AS adjusted") + assert(df.schema.fieldNames.toSeq === Seq("adjusted")) + // KNOWS: 1->2 (Alice,30), 2->1 (Bob,40). adjusted = {31, 41}. The arithmetic result column + // is LongType (integer column + integer literal promotes), so read it back as Long. + val adjusted = df.collect().map(_.getLong(0)).toSet + assert(adjusted === Set(31L, 41L)) + } + + test("RETURN * projects the fixed output schema") { + // Projection.Star is handled together with Projection.Default in QueryExecutor.project, so + // RETURN * must yield the same 6-column fixed schema and the same rows as the no-RETURN query. + val star = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN *") + assert( + star.schema.fieldNames === + Seq( + "start_id", + "start_property_group", + "end_id", + "end_property_group", + "edge_property_group", + "path")) + comparePaths( + star, + Seq( + ExpectedPath(1L, "Person", 2L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))), + ExpectedPath(2L, "Person", 1L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("single-node pattern returns each vertex with an empty path") { + // A 0-hop path (path.steps.isEmpty): the single node is both start and end, edge_property_group + // is null, and the path array is empty. Exercises the degenerate single-node branch of + // QueryExecutor.executePlan / project. + val df = pgf.query("MATCH (a:Person)") + assert(df.count() === 2) + val rows = df.collect() + rows.foreach { r => + assert(r.getAs[String]("start_property_group") === "Person") + assert(r.getAs[String]("end_property_group") === "Person") + // With only one named node, start and end both reference `a`. + assert(r.getAs[String]("start_id") === r.getAs[String]("end_id")) + assert(r.isNullAt(r.fieldIndex("edge_property_group"))) + val pathArr = r.get(r.fieldIndex("path")).asInstanceOf[scala.collection.Seq[_]] + assert(pathArr.isEmpty, s"0-hop path array must be empty: $r") + } + // The two vertices are Alice(1) and Bob(2). + val startIds = rows.map(_.getAs[String]("start_id")).toSet + assert(startIds === Set(maskedId(1L, "Person"), maskedId(2L, "Person"))) + } + + test("scan-local filter on the non-start variable prunes rows") { + // b.age > 35 references only `b`, so the resolver attaches it as a scan-local filter to the + // `b` node rather than the start `a`. Only Bob(40) passes as `b`; the only KNOWS edge into + // Bob is 1->2, so exactly Alice->Bob survives. + val df = pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE b.age > 35") + comparePaths( + df, + Seq(ExpectedPath(1L, "Person", 2L, "Person", "KNOWS", Seq(ExpectedHop.last("KNOWS"))))) + } + + test("non-adjacent post-filter spans the first and last variable") { + // A 3-variable predicate where a and c are not adjacent is classified as a post-filter and + // applied after the join tree. The 2-hop KNOWS->KNOWS chains are 1->2->1 and 2->1->2, so a + // and c are always the same vertex -- hence a.age > c.age is always false and the result is + // empty. (Without the post-filter, 2 rows would be returned, so this catches regressions + // where the post-filter is dropped.) + val df = + pgf.query("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) WHERE a.age > c.age") + assert(df.count() === 0) + } + + // --------------------------------------------------------------------------- + // Scan reuse: output-only property join-back (end-to-end through the public API). + // --------------------------------------------------------------------------- + + test("multi-hop output-only join-back resolves a Company property through RETURN") { + // `c.name` is RETURN-only (no filter references it) -> output-only -> terminal join-back on the + // masked id. WORKS_AT: (1,10) Alice -> Acme. So exactly one row, `name` = "Acme". (Per the + // Items-projection convention, the output column is named after the property, `name`.) + val df = pgf.query("MATCH (a:Person)-[:WORKS_AT]->(c:Company) RETURN c.name") + assert(df.schema.fieldNames.toSeq === Seq("name")) + val names = df.collect().map(_.getString(0)).toSet + assert(names === Set("Acme")) + } + + test("mixed carry + output-only resolves both a carried and a join-backed column end-to-end") { + // `a.age` is referenced by both the filter (`a.age >= 30`) and RETURN -> carried (no join-back). + // `a.name` is RETURN-only -> output-only -> join-backed. WORKS_AT only has Alice(1)->Acme, and + // Alice's age is 30, so `a.age >= 30` keeps exactly Alice. Result: one row (Alice, 30). The + // `age` column is read as Int (its physical type in the fixture). + val df = pgf.query( + "MATCH (a:Person)-[:WORKS_AT]->(c:Company) WHERE a.age >= 30 RETURN a.name, a.age") + val rows = df.collect().map(r => (r.getString(0), r.getInt(1))).toSet + assert(rows === Set(("Alice", 30))) + } +} diff --git a/core/src/test/scala/org/graphframes/propertygraph/PropertyGraphFrameTest.scala b/core/src/test/scala/org/graphframes/propertygraph/PropertyGraphFrameTest.scala index 88540ba93..427ef7183 100644 --- a/core/src/test/scala/org/graphframes/propertygraph/PropertyGraphFrameTest.scala +++ b/core/src/test/scala/org/graphframes/propertygraph/PropertyGraphFrameTest.scala @@ -249,6 +249,32 @@ class PropertyGraphFrameTest assert(projectedEdges === expectedEdges) } + test("schemaString returns human-readable schema description") { + val schema = peopleMoviesGraph.schemaString + + assert(schema.contains("people")) + assert(schema.contains("movies")) + assert(schema.contains("likes")) + assert(schema.contains("messages")) + assert(schema.startsWith("Property graph schema:")) + } + + test("schemaStringDOT returns valid DOT format") { + val dot = peopleMoviesGraph.schemaStringDOT + + assert(dot.startsWith("digraph SchemaGraph {")) + assert(dot.contains("\"people\"")) + assert(dot.contains("\"movies\"")) + assert(dot.contains("likes")) + assert(dot.contains("messages")) + assert(dot.trim().endsWith("}")) + } + + test("print schema") { + println(peopleMoviesGraph.schemaString) + println(peopleMoviesGraph.schemaStringDOT) + } + test("joinVertices withConnectedComponents") { // Convert to GraphFrame with all vertices and edges val graph = peopleMoviesGraph.toGraphFrame( diff --git a/core/src/test/scala/org/graphframes/propertygraph/internal/AstBuilderSuite.scala b/core/src/test/scala/org/graphframes/propertygraph/internal/AstBuilderSuite.scala new file mode 100644 index 000000000..d01ae8758 --- /dev/null +++ b/core/src/test/scala/org/graphframes/propertygraph/internal/AstBuilderSuite.scala @@ -0,0 +1,437 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +import org.graphframes.InvalidParseException +import org.graphframes.SparkFunSuite + +class AstBuilderSuite extends SparkFunSuite { + + test("typed node pattern with variable") { + val ast = AstBuilder.parse("MATCH (a:Person)") + val GraphPattern(Seq(NodePattern(Some("a"), Some("Person")))) = ast.pattern + assert(ast.where === None) + assert(ast.returnClause === None) + } + + test("untyped node pattern with variable") { + val ast = AstBuilder.parse("MATCH (x)") + val GraphPattern(Seq(NodePattern(Some("x"), None))) = ast.pattern + } + + test("anonymous node pattern") { + val ast = AstBuilder.parse("MATCH ()") + val GraphPattern(Seq(NodePattern(None, None))) = ast.pattern + } + + test("label-only node pattern") { + val ast = AstBuilder.parse("MATCH (:Person)") + val GraphPattern(Seq(NodePattern(None, Some("Person")))) = ast.pattern + } + + test("single directed edge right, typed") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val GraphPattern(elements) = ast.pattern + assert(elements.length === 3) + assert(elements(0) === NodePattern(Some("a"), Some("Person"))) + assert(elements(1) === EdgePattern(None, Some("KNOWS"), LeftToRight, None)) + assert(elements(2) === NodePattern(Some("b"), Some("Person"))) + } + + test("single directed edge left, with edge variable") { + val ast = AstBuilder.parse("MATCH (a)<-[e:KNOWS]-(b)") + val GraphPattern(Seq(_, EdgePattern(Some("e"), Some("KNOWS"), RightToLeft, None), _)) = + ast.pattern + } + + test("anonymous edge") { + val ast = AstBuilder.parse("MATCH (a:Person)-[]->(b:Person)") + val GraphPattern(Seq(_, EdgePattern(None, None, LeftToRight, None), _)) = ast.pattern + } + + test("multi-hop chain") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:WORKS_AT]->(c:Company)") + val GraphPattern(elements) = ast.pattern + assert(elements.length === 5) + assert(elements(2) === NodePattern(Some("b"), Some("Person"))) + assert(elements(3) === EdgePattern(None, Some("WORKS_AT"), LeftToRight, None)) + assert(elements(4) === NodePattern(Some("c"), Some("Company"))) + } + + test("WHERE comparison and AND") { + val ast = AstBuilder.parse("MATCH (a:Person) WHERE a.age > 30 AND a.name = 'Bob'") + val Some( + And( + Comparison(PropertyAccess("a", "age"), Gt, Literal(30L)), + Comparison(PropertyAccess("a", "name"), Eq, Literal("Bob")))) = ast.where + } + + test("WHERE OR and NOT and parentheses") { + val ast = AstBuilder.parse("MATCH (a:Person) WHERE NOT (a.age > 30) OR a.active = TRUE") + val Some( + Or( + Not(Comparison(PropertyAccess("a", "age"), Gt, Literal(30L))), + Comparison(PropertyAccess("a", "active"), Eq, Literal(true)))) = ast.where + } + + test("WHERE cross-pattern predicate") { + val ast = AstBuilder.parse("MATCH (a)-[:KNOWS]->(b) WHERE a.age > b.age") + val Some(Comparison(PropertyAccess("a", "age"), Gt, PropertyAccess("b", "age"))) = ast.where + } + + test("WHERE additive expression") { + val ast = AstBuilder.parse("MATCH (a) WHERE a.age + 1 > 30") + val Some( + Comparison(Arithmetic(PropertyAccess("a", "age"), Plus, Literal(1L)), Gt, Literal(30L))) = + ast.where + } + + test("WHERE subtraction additive chain is left-associative") { + val ast = AstBuilder.parse("MATCH (a) WHERE a.x - a.y - 1 = 0") + val Some( + Comparison( + Arithmetic( + Arithmetic(PropertyAccess("a", "x"), Minus, PropertyAccess("a", "y")), + Minus, + Literal(1L)), + Eq, + Literal(0L))) = ast.where + } + + test("both <> and != map to Neq") { + val a1 = AstBuilder.parse("MATCH (a) WHERE a.x <> 1") + val a2 = AstBuilder.parse("MATCH (a) WHERE a.x != 1") + assert(a1.where === Some(Comparison(PropertyAccess("a", "x"), Neq, Literal(1L)))) + assert(a2.where === a1.where) + } + + test("RETURN items and alias") { + val ast = AstBuilder.parse("MATCH (a:Person) RETURN a, a.name AS person_name") + val Some( + ReturnItems( + Seq( + ReturnItem(Variable("a"), None), + ReturnItem(PropertyAccess("a", "name"), Some("person_name"))))) = ast.returnClause + } + + test("RETURN star") { + val ast = AstBuilder.parse("MATCH (a:Person) RETURN *") + assert(ast.returnClause === Some(ReturnStar)) + } + + test("RETURN omitted is parsed as None") { + val ast = AstBuilder.parse("MATCH (a:Person)") + assert(ast.returnClause === None) + } + + test("keywords are case-insensitive") { + val ast = AstBuilder.parse("match (a:Person) where a.age > 30 return a") + assert(ast.where.isDefined) + assert(ast.returnClause === Some(ReturnItems(Seq(ReturnItem(Variable("a"), None))))) + } + + test("string literal with '' escape") { + val ast = AstBuilder.parse("MATCH (a) WHERE a.name = 'O''Brien'") + val Some(Comparison(PropertyAccess("a", "name"), Eq, Literal("O'Brien"))) = ast.where + } + + test("decimal literal") { + val ast = AstBuilder.parse("MATCH (a) WHERE a.score > 3.14") + val Some(Comparison(PropertyAccess("a", "score"), Gt, Literal(3.14))) = ast.where + } + + test("line and block comments are skipped") { + val ast = AstBuilder.parse("""MATCH (a:Person) // trailing line comment + |/* block + | comment */ WHERE a.age > 30 RETURN a + |""".stripMargin) + assert(ast.where.isDefined) + assert(ast.returnClause.isDefined) + } + + test("bare variable in RETURN") { + val ast = AstBuilder.parse("MATCH (a:Person) RETURN a") + val Some(ReturnItems(Seq(ReturnItem(Variable("a"), None)))) = ast.returnClause + } + + // ----------------------------------------------------------------------- + // Reject cases (out-of-scope constructs must throw InvalidParseException). + // ----------------------------------------------------------------------- + test("reject OPTIONAL MATCH") { + intercept[InvalidParseException] { + AstBuilder.parse("OPTIONAL MATCH (a:Person)") + } + } + + test("reject ORDER BY") { + intercept[InvalidParseException] { + AstBuilder.parse("MATCH (a:Person) RETURN a ORDER BY a.name") + } + } + + test("reject empty input") { + intercept[InvalidParseException] { + AstBuilder.parse("") + } + } + + test("reject edge with missing destination node") { + intercept[InvalidParseException] { + AstBuilder.parse("MATCH (a)-[e:KNOWS]->") + } + } + + test("single undirected edge, typed") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]-(b:Person)") + val GraphPattern(elements) = ast.pattern + assert(elements(1) === EdgePattern(None, Some("KNOWS"), Undirected, None)) + } + + test("undirected edge with variable") { + val ast = AstBuilder.parse("MATCH (a)-[e:KNOWS]-(b)") + val GraphPattern(Seq(_, EdgePattern(Some("e"), Some("KNOWS"), Undirected, None), _)) = + ast.pattern + } + + test("anonymous undirected edge") { + val ast = AstBuilder.parse("MATCH (a:Person)-[]-(b:Person)") + val GraphPattern(Seq(_, EdgePattern(None, None, Undirected, None), _)) = ast.pattern + } + + test("undirected edge in a multi-hop chain mixes with directed arrows") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]-(b:Person)-[:WORKS_AT]->(c:Company)") + val GraphPattern(elements) = ast.pattern + assert(elements(1) === EdgePattern(None, Some("KNOWS"), Undirected, None)) + assert(elements(3) === EdgePattern(None, Some("WORKS_AT"), LeftToRight, None)) + } + + // ----------------------------------------------------------------------- + // Scalar function calls (datetime family). + // ----------------------------------------------------------------------- + test("function call over a property access") { + val ast = AstBuilder.parse("MATCH (a:Person) WHERE year(a.creationDate) = 2012") + val Some( + Comparison( + FunctionCall("year", Seq(PropertyAccess("a", "creationDate"))), + Eq, + Literal(2012L))) = + ast.where + } + + test("function call with multiple arguments") { + val ast = AstBuilder.parse("MATCH (a)-[:R]->(b) WHERE datediff(a.d, b.d) > 30") + val Some( + Comparison( + FunctionCall("datediff", Seq(PropertyAccess("a", "d"), PropertyAccess("b", "d"))), + Gt, + Literal(30L))) = ast.where + } + + test("nested function calls inside arithmetic and comparison") { + val ast = AstBuilder.parse("MATCH (a)-[:R]->(b) WHERE year(a.d) - year(b.d) > 1") + val Some( + Comparison( + Arithmetic( + FunctionCall("year", Seq(PropertyAccess("a", "d"))), + Minus, + FunctionCall("year", Seq(PropertyAccess("b", "d")))), + Gt, + Literal(1L))) = ast.where + } + + test("function call over a string literal argument") { + val ast = AstBuilder.parse("MATCH (a) WHERE a.d = date('2012-06-01')") + val Some( + Comparison( + PropertyAccess("a", "d"), + Eq, + FunctionCall("date", Seq(Literal("2012-06-01"))))) = ast.where + } + + test("zero-argument function call") { + val ast = AstBuilder.parse("MATCH (a) WHERE a.d < current_timestamp()") + val Some(Comparison(PropertyAccess("a", "d"), Lt, FunctionCall("current_timestamp", Seq()))) = + ast.where + } + + test("function name and same-named property coexist in one query") { + // `a.date` is a property access; `date(...)` is a function call. The parser disambiguates by + // the token following the IDENTIFIER (DOT vs LPAREN). + val ast = AstBuilder.parse("MATCH (a) WHERE date(a.date) = date('2012-06-01')") + val Some( + Comparison( + FunctionCall("date", Seq(PropertyAccess("a", "date"))), + Eq, + FunctionCall("date", Seq(Literal("2012-06-01"))))) = ast.where + } + + test("RETURN of a function call") { + val ast = AstBuilder.parse("MATCH (a:Person) RETURN year(a.creationDate) AS y") + val Some( + ReturnItems(Seq( + ReturnItem(FunctionCall("year", Seq(PropertyAccess("a", "creationDate"))), Some("y"))))) = + ast.returnClause + } + + test("unknown function name still parses (grammar allows any identifier)") { + // `frobnicate` is not in the whitelist, but the grammar accepts any IDENTIFIER as a function + // name; rejection happens at lowering, not parsing. + val ast = AstBuilder.parse("MATCH (a) WHERE frobnicate(a.x) = 1") + val Some( + Comparison(FunctionCall("frobnicate", Seq(PropertyAccess("a", "x"))), Eq, Literal(1L))) = + ast.where + } + + test("referencedVariables recurses into function-call arguments") { + // Regression guard for the §4 traversal edit: a function call must contribute the variables + // referenced by its arguments (this is what makes the resolver classify + // `WHERE year(a.creationDate) = 2012` as scan-local on `a`). + val expr = FunctionCall("year", Seq(PropertyAccess("a", "creationDate"))) + assert(GqlAst.referencedVariables(expr) === Set("a")) + } + + test("lowering rejects an unknown function name") { + // Rejection of unsupported names happens at lowering, not parse time. + intercept[UnsupportedOperationException] { + ExpressionLowering.lower( + FunctionCall("frobnicate", Seq(PropertyAccess("a", "x"))), + PrefixEnv.raw) + } + } + + test("lowering rejects wrong function arity") { + intercept[UnsupportedOperationException] { + ExpressionLowering.lower( + FunctionCall("year", Seq(PropertyAccess("a", "x"), PropertyAccess("a", "y"))), + PrefixEnv.raw) + } + } + + // ----------------------------------------------------------------------- + // Multiplicative operators (*, /, %) -- precedence and shape. + // ----------------------------------------------------------------------- + test("multiplicative * binds tighter than additive +") { + // a.x + a.y * a.z parses as (a.x + (a.y * a.z)). + val ast = AstBuilder.parse("MATCH (a) WHERE a.x + a.y * a.z = 1") + val Some( + Comparison( + Arithmetic( + PropertyAccess("a", "x"), + Plus, + Arithmetic(PropertyAccess("a", "y"), Mult, PropertyAccess("a", "z"))), + Eq, + Literal(1L))) = ast.where + } + + test("multiplicative % over a property and a literal") { + val ast = AstBuilder.parse("MATCH (a) WHERE a.x % 512 = 0") + val Some( + Comparison(Arithmetic(PropertyAccess("a", "x"), Mod, Literal(512L)), Eq, Literal(0L))) = + ast.where + } + + test("multiplicative chain is left-associative") { + // a.x / a.y / a.z -> ((a.x / a.y) / a.z) + val ast = AstBuilder.parse("MATCH (a) WHERE a.x / a.y / a.z = 1") + val Some( + Comparison( + Arithmetic( + Arithmetic(PropertyAccess("a", "x"), Div, PropertyAccess("a", "y")), + Div, + PropertyAccess("a", "z")), + Eq, + Literal(1L))) = ast.where + } + + test("mixed additive and multiplicative precedence with parentheses") { + // (a.x + a.y) * 2 -> Arithmetic( (a.x + a.y), Mult, 2 ) + val ast = AstBuilder.parse("MATCH (a) WHERE (a.x + a.y) * 2 = 1") + val Some( + Comparison( + Arithmetic( + Arithmetic(PropertyAccess("a", "x"), Plus, PropertyAccess("a", "y")), + Mult, + Literal(2L)), + Eq, + Literal(1L))) = ast.where + } + + // ----------------------------------------------------------------------- + // Scalar function calls (string/math/json/xml/hash families). + // ----------------------------------------------------------------------- + test("variadic function call preserves all arguments") { + val ast = AstBuilder.parse("MATCH (a) WHERE coalesce(a.x, a.y, a.z) = 1") + val Some( + Comparison( + FunctionCall( + "coalesce", + Seq(PropertyAccess("a", "x"), PropertyAccess("a", "y"), PropertyAccess("a", "z"))), + Eq, + Literal(1L))) = ast.where + } + + test("string-literal function argument parses as Literal") { + val ast = AstBuilder.parse("MATCH (a) WHERE get_json_object(a.p, '$.k') = 'x'") + val Some( + Comparison( + FunctionCall("get_json_object", Seq(PropertyAccess("a", "p"), Literal("$.k"))), + Eq, + Literal("x"))) = ast.where + } + + test("nested arithmetic inside a function call") { + // pmod(hash(a.id), 512) = 0 -- the sampling idiom. + val ast = AstBuilder.parse("MATCH (a) WHERE pmod(hash(a.id), 512) = 0") + val Some( + Comparison( + FunctionCall( + "pmod", + Seq(FunctionCall("hash", Seq(PropertyAccess("a", "id"))), Literal(512L))), + Eq, + Literal(0L))) = ast.where + } + + test("lowering accepts a variadic function with many args") { + // greatest is variadic (>=2); three args must lower without an arity error. + ExpressionLowering.lower( + FunctionCall( + "greatest", + Seq(PropertyAccess("a", "x"), PropertyAccess("a", "y"), PropertyAccess("a", "z"))), + PrefixEnv.raw) + } + + test("lowering rejects a property reference where a string literal is required") { + // regexp_extract wants (col, lit-str, lit-int); passing a property as the pattern must fail. + intercept[UnsupportedOperationException] { + ExpressionLowering.lower( + FunctionCall( + "regexp_extract", + Seq(PropertyAccess("a", "s"), PropertyAccess("a", "p"), Literal(1L))), + PrefixEnv.raw) + } + } + + test("lowering rejects a property reference where an integer literal is required") { + // sha2 wants (col, lit-int); passing a property as the bit length must fail. + intercept[UnsupportedOperationException] { + ExpressionLowering.lower( + FunctionCall("sha2", Seq(PropertyAccess("a", "s"), PropertyAccess("a", "bits"))), + PrefixEnv.raw) + } + } +} diff --git a/core/src/test/scala/org/graphframes/propertygraph/internal/GqlExplainSuite.scala b/core/src/test/scala/org/graphframes/propertygraph/internal/GqlExplainSuite.scala new file mode 100644 index 000000000..3c5490811 --- /dev/null +++ b/core/src/test/scala/org/graphframes/propertygraph/internal/GqlExplainSuite.scala @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +import org.graphframes.SparkFunSuite +import org.graphframes.propertygraph.QueryOptions + +/** + * Pure-JVM tests for the explain renderers. No SparkSession required; the renderers are pure + * functions over the resolved IR values. + */ +class GqlExplainSuite extends SparkFunSuite { + + private val schema = SchemaGraphSnapshot( + vertexGroupNames = Set("Person", "Company", "City"), + edges = Vector( + SchemaEdge("KNOWS", "Person", "Person", true), + SchemaEdge("WORKS_AT", "Person", "Company", true), + SchemaEdge("LOCATED_IN", "Company", "City", true))) + + private val options = QueryOptions() + + test("logical explain renders the path with a forward arrow") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + val out = GqlExplain.logical(rq) + assert(out.contains("Logical plan")) + assert(out.contains("(a:Person)")) + // Anonymous edge renders without a variable prefix: -[KNOWS]-> + assert(out.contains("-[KNOWS]->")) + } + + test("logical explain renders a backward arrow for <-[e]- ") { + val ast = AstBuilder.parse("MATCH (a:Person)<-[:KNOWS]-(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + val out = GqlExplain.logical(rq) + assert(out.contains("<-[KNOWS]-")) + } + + test("logical explain reports disconnected patterns as (none)") { + val ast = AstBuilder.parse("MATCH (a:City)-[:KNOWS]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.isEmpty) + val out = GqlExplain.logical(rq) + assert(out.contains("(none")) + } + + test( + "logical explain lists scan-local filter on the node and join/post predicates separately") { + val ast = AstBuilder.parse( + "MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > 30 AND a.age > b.age RETURN a, b") + val rq = Resolver.resolve(ast, schema, options) + val out = GqlExplain.logical(rq) + // scan-local filter rendered inline on the node + assert(out.contains("a.age > 30")) + // the cross-pattern predicate rendered as a join predicate + assert(out.contains("a.age > b.age")) + assert(out.contains("join predicates")) + assert(out.contains("projection")) + } + + test("physical explain renders plan order and statistics line") { + val ast = + AstBuilder.parse("MATCH (a:Person)-[:WORKS_AT]->(c:Company)-[:LOCATED_IN]->(d:City)") + val rq = Resolver.resolve(ast, schema, options) + val plans = JoinOptimizer.plan(rq, stats = None) + val out = GqlExplain.physical(plans) + assert(out.contains("Physical plan")) + assert(out.contains("Plan 0")) + assert(out.contains("join order: [n0, e0, n1, e1, n2]")) + // v1 carries no statistics. + assert(out.contains("(no statistics)")) + } + + test("physical explain on disconnected pattern reports no plans") { + val ast = AstBuilder.parse("MATCH (a:City)-[:KNOWS]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + val plans = JoinOptimizer.plan(rq, stats = None) + val out = GqlExplain.physical(plans) + assert(out.contains("(none")) + } +} diff --git a/core/src/test/scala/org/graphframes/propertygraph/internal/JoinOptimizerSuite.scala b/core/src/test/scala/org/graphframes/propertygraph/internal/JoinOptimizerSuite.scala new file mode 100644 index 000000000..98dc42a53 --- /dev/null +++ b/core/src/test/scala/org/graphframes/propertygraph/internal/JoinOptimizerSuite.scala @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +import org.graphframes.SparkFunSuite +import org.graphframes.propertygraph.QueryOptions + +/** + * Pure-JVM tests for `JoinOptimizer.plan`. + */ +class JoinOptimizerSuite extends SparkFunSuite { + + // Person --KNOWS--> Person + // Person --WORKS_AT--> Company + // Company --LOCATED_IN--> City + private val schema = SchemaGraphSnapshot( + vertexGroupNames = Set("Person", "Company", "City"), + edges = Vector( + SchemaEdge("KNOWS", "Person", "Person", true), + SchemaEdge("WORKS_AT", "Person", "Company", true), + SchemaEdge("LOCATED_IN", "Company", "City", true))) + + val options: QueryOptions = QueryOptions() + + test("single-hop path yields one plan in pattern order n0,e0,n1") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + val plans = JoinOptimizer.plan(rq, stats = None) + + assert(plans.length === 1) + val plan = plans.head + assert(plan.order === Vector(NodeRef(0), EdgeRef(0), NodeRef(1))) + assert(plan.statsUsed === Map.empty) + assert(plan.projection === Projection.Default) + assert(plan.joinPredicates === Nil) + assert(plan.postFilters === Nil) + } + + test("multi-hop pattern order interleaves nodes and edges") { + val ast = + AstBuilder.parse("MATCH (a:Person)-[:WORKS_AT]->(c:Company)-[:LOCATED_IN]->(d:City)") + val rq = Resolver.resolve(ast, schema, options) + val plans = JoinOptimizer.plan(rq, stats = None) + + assert(plans.length === 1) + val order = plans.head.order + assert(order === Vector(NodeRef(0), EdgeRef(0), NodeRef(1), EdgeRef(1), NodeRef(2))) + } + + test("untyped fan-out produces one plan per enumerated path") { + val ast = AstBuilder.parse("MATCH (a:Person)-[]->(x)-[]->(b:City)") + val rq = Resolver.resolve(ast, schema, options) + val plans = JoinOptimizer.plan(rq, stats = None) + + // Only Person-WORKS_AT->Company-LOCATED_IN->City reaches City in two hops. + assert(plans.length === rq.paths.length) + assert(plans.length >= 1) + plans.foreach { p => + assert(p.order.head === NodeRef(0)) + assert(p.order.last === NodeRef(2)) + } + } + + test("predicates and projection are carried through to each plan") { + val ast = + AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > b.age RETURN a, b") + val rq = Resolver.resolve(ast, schema, options) + val plans = JoinOptimizer.plan(rq, stats = None) + + assert(plans.length === 1) + val plan = plans.head + assert( + plan.projection === Projection.Items(rq.projection.asInstanceOf[Projection.Items].items)) + // `a.age > b.age` spans two adjacent node vars -> classified as a join predicate. + assert(plan.joinPredicates.length === 1) + assert(plan.postFilters === Nil) + } + + test("disconnected pattern (no paths) yields no plans") { + // Company and Person are not connected by any incoming edge into Company from City, etc. + // Construct a pattern that cannot resolve: City ->(none)-> Person in one hop is impossible + // because no edge has City as src reaching Person. Use a label-only dead end. + val ast = AstBuilder.parse("MATCH (a:City)-[:KNOWS]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.isEmpty) + val plans = JoinOptimizer.plan(rq, stats = None) + assert(plans.isEmpty) + } + + test("stats argument is accepted but does not change v1 output") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + val withStats = JoinOptimizer.plan(rq, Some(GraphStatistics.Empty)) + val withoutStats = JoinOptimizer.plan(rq, None) + assert(withStats.map(_.order) === withoutStats.map(_.order)) + assert(withStats.head.statsUsed === Map.empty) + } + + test("defaultPlanner and identityRefiner compose to the same as plan()") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + val direct = JoinOptimizer.plan(rq, None) + val viaSPI = JoinOptimizer.identityRefiner(rq, JoinOptimizer.defaultPlanner(rq, None)) + assert(viaSPI.map(_.order) === direct.map(_.order)) + } +} diff --git a/core/src/test/scala/org/graphframes/propertygraph/internal/QueryExecutorSuite.scala b/core/src/test/scala/org/graphframes/propertygraph/internal/QueryExecutorSuite.scala new file mode 100644 index 000000000..30e1966bc --- /dev/null +++ b/core/src/test/scala/org/graphframes/propertygraph/internal/QueryExecutorSuite.scala @@ -0,0 +1,770 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +import org.apache.spark.sql.DataFrame +import org.apache.spark.sql.functions.lit +import org.graphframes.GraphFrame +import org.graphframes.GraphFrameTestSparkContext +import org.graphframes.SparkFunSuite +import org.graphframes.propertygraph.PropertyGraphFrame +import org.graphframes.propertygraph.QueryOptions +import org.graphframes.propertygraph.property.EdgePropertyGroup +import org.graphframes.propertygraph.property.VertexPropertyGroup + +import java.security.MessageDigest + +/** + * Spark-backed tests for the executor + optimizer pipeline. Builds a small in-memory + * PropertyGraphFrame (Person/Company/City, KNOWS/WORKS_AT/LOCATED_IN) and asserts that + * `JoinOptimizer.plan` + `QueryExecutor.execute` produce rows matching a hand-computed join, with + * the fixed output schema and correct id-masking. + */ +class QueryExecutorSuite extends SparkFunSuite with GraphFrameTestSparkContext { + + import sqlImplicits._ + + private var pgf: PropertyGraphFrame = _ + + // Mirrors VertexPropertyGroup.getData's masking: concat(groupName, sha2(id.cast(String), 256)). + private def maskedId(id: Long, groupName: String): String = { + val md = MessageDigest.getInstance("SHA-256") + val hash = md.digest(id.toString.getBytes("UTF-8")).map("%02x".format(_)).mkString + s"$groupName$hash" + } + + override def beforeAll(): Unit = { + super.beforeAll() + pgf = buildGraph() + } + + private def buildGraph(): PropertyGraphFrame = { + val persons = + Seq((1L, "Alice", 30), (2L, "Bob", 40), (3L, "Carol", 25)).toDF("id", "name", "age") + val companies = Seq((10L, "Acme"), (20L, "Globex")).toDF("id", "name") + val cities = Seq((100L, "Springfield"), (200L, "Shelbyville")).toDF("id", "name") + + val personGroup = VertexPropertyGroup("Person", persons, "id") + val companyGroup = VertexPropertyGroup("Company", companies, "id") + val cityGroup = VertexPropertyGroup("City", cities, "id") + + val knows = Seq((1L, 2L, "friend"), (2L, 3L, "collegaue"), (3L, 1L, "spoose")).toDF( + "src", + "dst", + "friendship") + val worksAt = Seq((1L, 10L), (2L, 10L), (3L, 20L)).toDF("src", "dst") + val locatedIn = Seq((10L, 100L), (20L, 200L)).toDF("src", "dst") + + val knowsGroup = EdgePropertyGroup( + "KNOWS", + knows, + personGroup, + personGroup, + isDirected = true, + "src", + "dst", + lit(1.0)) + val worksAtGroup = EdgePropertyGroup( + "WORKS_AT", + worksAt, + personGroup, + companyGroup, + isDirected = true, + "src", + "dst", + lit(1.0)) + val locatedInGroup = EdgePropertyGroup( + "LOCATED_IN", + locatedIn, + companyGroup, + cityGroup, + isDirected = true, + "src", + "dst", + lit(1.0)) + + PropertyGraphFrame( + Seq(personGroup, companyGroup, cityGroup), + Seq(knowsGroup, worksAtGroup, locatedInGroup)) + } + + // this one is left just because I was lazy to rewrite all the tests :D + private def run(gql: String): DataFrame = runOn(pgf, gql) + + private def runOn(pg: PropertyGraphFrame, gql: String): DataFrame = { + val ast = AstBuilder.parse(gql) + val resolved = + Resolver.resolve(ast, SchemaGraphSnapshot.fromPropertyGraphFrame(pg), QueryOptions()) + val plans = JoinOptimizer.plan(resolved, stats = None) + QueryExecutor.execute(pg, plans) + } + + test("single-hop directed query matches a hand-computed join") { + // MATCH (a:Person)-[:KNOWS]->(b:Person) : knows rows (1->2),(2->3),(3->1). + val df = run("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val rows = df.collect().map(r => (r.getString(0), r.getString(2))).toSet + val expected = Set( + (maskedId(1L, "Person"), maskedId(2L, "Person")), + (maskedId(2L, "Person"), maskedId(3L, "Person")), + (maskedId(3L, "Person"), maskedId(1L, "Person"))) + assert(rows === expected) + // Fixed schema check. + assert( + df.schema.fieldNames === + Seq( + "start_id", + "start_property_group", + "end_id", + "end_property_group", + "edge_property_group", + "path")) + assert(df.head().getAs[String]("start_property_group") === "Person") + assert(df.head().getAs[String]("edge_property_group") === "KNOWS") + } + + test("scan-local WHERE filter actually prunes rows") { + // Only Alice(30) and Bob(40) have age > 30... wait: 30 > 30 is false. Only Bob(40). + // As a src, Bob(2) KNOWS Carol(3). So exactly one row. + val df = run("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > 30") + assert(df.count() === 1) + val row = df.head() + assert(row.getString(0) === maskedId(2L, "Person")) // Bob + assert(row.getString(2) === maskedId(3L, "Person")) // Carol + } + + test("backward arrow <-[e]- swaps src and dst") { + // MATCH (a:Person)<-[:KNOWS]-(b:Person): a is the dst, b is the src. + // knows (1->2),(2->3),(3->1) => a=2,b=1 ; a=3,b=2 ; a=1,b=3. + val df = run("MATCH (a:Person)<-[:KNOWS]-(b:Person)") + val rows = df.collect().map(r => (r.getString(0), r.getString(2))).toSet + val expected = Set( + (maskedId(2L, "Person"), maskedId(1L, "Person")), + (maskedId(3L, "Person"), maskedId(2L, "Person")), + (maskedId(1L, "Person"), maskedId(3L, "Person"))) + assert(rows === expected) + } + + test("multi-hop query builds the path array with intermediate ids") { + // MATCH (a:Person)-[:WORKS_AT]->(c:Company)-[:LOCATED_IN]->(d:City) + // Alice(1)->Acme(10)->Springfield(100), Bob(2)->Acme(10)->Springfield(100), + // Carol(3)->Globex(20)->Shelbyville(200). + val df = run("MATCH (a:Person)-[:WORKS_AT]->(c:Company)-[:LOCATED_IN]->(d:City)") + assert(df.count() === 3) + val firstRow = df.head() + assert(firstRow.getAs[String]("start_property_group") === "Person") + assert(firstRow.getAs[String]("end_property_group") === "City") + assert(firstRow.getAs[String]("edge_property_group") === "WORKS_AT") + // path array: k entries for a k-step path (§6.1). Here k=2: + // [ {WORKS_AT, c_id, "Company"}, {LOCATED_IN, null, null} ] + // -- the last entry carries only the edge group (the end node is in end_id). + val pathArr = firstRow + .get(firstRow.fieldIndex("path")) + .asInstanceOf[scala.collection.Seq[org.apache.spark.sql.Row]] + assert(pathArr.length === 2) + val firstHop = pathArr.head + assert(firstHop.getAs[String](0) === "WORKS_AT") + assert(firstHop.getAs[String](2) === "Company") + val lastHop = pathArr.last + assert(lastHop.getAs[String](0) === "LOCATED_IN") + // last entry's node fields are null (end node already in end_id) + assert(lastHop.isNullAt(1)) + assert(lastHop.isNullAt(2)) + } + + test("disconnected pattern yields an empty DataFrame with the fixed schema") { + val df = run("MATCH (a:City)-[:KNOWS]->(b:Person)") + assert(df.count() === 0) + assert( + df.schema.fieldNames === + Seq( + "start_id", + "start_property_group", + "end_id", + "end_property_group", + "edge_property_group", + "path")) + } + + test("untyped edge fan-out unions multiple schema paths") { + // MATCH (a:Person)-[]->(b:Person) : only KNOWS connects Person->Person. + // So exactly the 3 KNOWS rows. + val df = run("MATCH (a:Person)-[]->(b:Person)") + assert(df.count() === 3) + } + + test("RETURN a, b projects named variables only") { + val df = run("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b") + // Items projection: two columns named a, b (each is the masked id). + assert(df.schema.fieldNames.toSeq === Seq("a", "b")) + val rows = df.collect().map(r => (r.getString(0), r.getString(1))).toSet + val expected = Set( + (maskedId(1L, "Person"), maskedId(2L, "Person")), + (maskedId(2L, "Person"), maskedId(3L, "Person")), + (maskedId(3L, "Person"), maskedId(1L, "Person"))) + assert(rows === expected) + } + + test("RETURN a.name AS age projects a property column via the requested-properties scan") { + // RETURN a.name : names are Alice/Bob/Carol. As src of KNOWS: 1->Alice,2->Bob,3->Carol. + val df = run("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name AS who") + assert(df.schema.fieldNames.toSeq === Seq("who")) + val names = df.collect().map(_.getString(0)).toSet + assert(names === Set("Alice", "Bob", "Carol")) + } + + test("join predicate (a.age > b.age) is applied") { + // KNOWS: 1(30)->2(40), 2(40)->3(25), 3(25)->1(30). + // a.age > b.age : 40>25 (Bob->Carol) only. + val df = run("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > b.age") + assert(df.count() === 1) + val row = df.head() + assert(row.getString(0) === maskedId(2L, "Person")) + assert(row.getString(2) === maskedId(3L, "Person")) + } + + test("id-masking join-back via internalIdMapping recovers raw ids") { + // The query returns masked ids; joining against Person.internalIdMapping recovers external ids. + val df = run("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val personGroup = pgf.vertexGroups("person") + val mapping = personGroup.internalIdMapping // (external_id, id) + val withExternal = df.join(mapping, df("start_id") === mapping(GraphFrame.ID), "left") + val externals = + withExternal.collect().map(_.getAs[Long](PropertyGraphFrame.EXTERNAL_ID)).toSet + assert(externals === Set(1L, 2L, 3L)) + } + + test("hand-computed join matches executor for the multi-hop case") { + // Independent computation of the expected start/end id pairs for the WORKS_AT->LOCATED_IN chain. + val worksAt = Seq((1L, 10L), (2L, 10L), (3L, 20L)) + val locatedIn = Seq((10L, 100L), (20L, 200L)) + val expected = for { + (p, c1) <- worksAt + (c2, ci) <- locatedIn if c1 == c2 + } yield (maskedId(p, "Person"), maskedId(ci, "City")) + + val df = run("MATCH (a:Person)-[:WORKS_AT]->(c:Company)-[:LOCATED_IN]->(d:City)") + val actual = df.collect().map(r => (r.getString(0), r.getString(2))).toSet + assert(actual === expected.toSet) + } + + // --------------------------------------------------------------------------- + // Scan-reuse floor (deterministic) + ceiling (best-effort / soft). + // --------------------------------------------------------------------------- + + /** + * Drives the same pipeline as [[run]] but also returns the per-call scan memo, so the + * scan-reuse floor can be asserted by reference-identity on the memo values. + */ + private def runWithMemo(gql: String): (DataFrame, Map[QueryExecutor.ScanKey, DataFrame]) = { + val ast = AstBuilder.parse(gql) + val resolved = + Resolver.resolve(ast, SchemaGraphSnapshot.fromPropertyGraphFrame(pgf), QueryOptions()) + val plans = JoinOptimizer.plan(resolved, stats = None) + QueryExecutor.executeWithScanMemo(pgf, plans) + } + + test("scan-reuse floor: equal scan signatures share one DataFrame reference (spec §8.1)") { + // KNOWS connects Person->Person, so both endpoints of `MATCH (a:Person)-[:KNOWS]->(b:Person)` + // are the SAME group (Person) with the SAME signature (empty scan filter, no carried cols). + // The memo must therefore hold a single Person scan referenced from both positions. + val (_, memo) = runWithMemo("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val personScans = memo.iterator.filter(_._1.groupName == "person").map(_._2).toSeq + assert(personScans.length === 1, s"expected one shared Person scan, got keys: ${memo.keySet}") + } + + test("scan-reuse floor: differing scan filters produce distinct scans (spec §8.1)") { + // With a scan-local filter `WHERE a.age > 30`, the `a` Person scan's signature differs from the + // `b` Person scan (no filter) -> two distinct Person scans in the memo. + val (_, memo) = runWithMemo("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > 30") + val personKeys = memo.keySet.filter(_.groupName == "person").toSeq + assert( + personKeys.length === 2, + s"expected two Person scans (filtered vs unfiltered): $personKeys") + // And the two scans must be distinct DataFrame references. + val personScans = personKeys.map(memo) + assert(personScans.head ne personScans.last, "distinct signatures must yield distinct scans") + } + + test("scan-reuse floor: same filter across a fan-out reuses one scan (spec §8.1)") { + // `(a:Person)-[]->(b:Person)` fans out; here only KNOWS qualifies, but both endpoints share the + // Person scan with no filter -> exactly one Person scan regardless. + val (_, memo) = runWithMemo("MATCH (a:Person)-[]->(b:Person)") + val personScans = memo.iterator.filter(_._1.groupName == "person").map(_._2).toSeq + assert(personScans.length === 1, s"expected one shared Person scan: ${memo.keySet}") + } + + test("output-only join-back resolves both endpoints' properties (spec §8.3)") { + // `a.name` and `b.name` are RETURN-only (no filter references them) -> output-only -> terminal + // join-back. Result parity: exact name pairs for KNOWS (1->2, 2->3, 3->1). Per the + // Items-projection convention the output columns are named after the property (`name`). + val df = run("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name") + assert(df.schema.fieldNames.toSeq === Seq("name", "name")) + val rows = df.collect().map(r => (r.getString(0), r.getString(1))).toSet + val expected = Set(("Alice", "Bob"), ("Bob", "Carol"), ("Carol", "Alice")) + assert(rows === expected) + } + + test("mixed carry + output-only: filter-also-returned is carried, RETURN-only is join-backed") { + // `a.age` is referenced by BOTH the filter (`a.age > 30`) and RETURN -> carried (no join-back). + // `a.name` is RETURN-only -> output-only -> join-backed. Only Bob(40) passes `age > 30`. The + // `age` column is read as Int (its physical type in the fixture), not Long. + val df = run("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > 30 RETURN a.name, a.age") + val rows = df.collect().map(r => (r.getString(0), r.getInt(1))).toSet + assert(rows === Set(("Bob", 40))) + } + + test("Default/Star projection never triggers an output-only join-back") { + // Default projection has no RETURN items -> every element's outputOnly set is empty by + // construction -> no join-back. Assert via the memo: no carried scan carries a non-id-only + // property set, and the result still has the fixed 6-column schema. + val (df, memo) = runWithMemo("MATCH (a:Person)-[:KNOWS]->(b:Person)") + assert( + df.schema.fieldNames === Seq( + "start_id", + "start_property_group", + "end_id", + "end_property_group", + "edge_property_group", + "path")) + // No scan in the memo carried any property column (only id/property_group/src/dst/weight). + assert( + memo.keySet.forall(_.carriedCols.isEmpty), + s"Default projection should carry no props: ${memo.keySet}") + } + + test("selectivity preserved: join predicate appears in the executed plan") { + // `a.age > b.age` is a two-variable predicate; the engine places it at the binding join. This is + // a best-effort ceiling assertion: the inequality Column must appear SOMEWHERE in the physical + // plan (exact operator placement is Spark-version / AQE dependent). We assert only presence. + val df = run("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > b.age") + val planStr = df.queryExecution.executedPlan.toString() + // The predicate is struct guaranteed to be applied; assert the result parity strictly and the + // plan presence softly (only one row: 40 > 25, Bob -> Carol). + assert(df.count() === 1) + // Soft: tolerate plans that fold the comparison into a different node name across versions. + assert( + planStr.contains("age") || planStr.contains("Filter") || planStr.contains("Join"), + s"expected the age predicate or a join/filter node in the plan, got:\n$planStr") + } + + test("edge properties in the filter expression are carried correctly") { + val df = run("MATCH (a:Person)-[e:KNOWS]->(b:Person) WHERE e.friendship = 'spoose'") + // only Carol -- Alice + assert(df.count() === 1L) + } + + test("only edge property referenced only in RETURN") { + val df = run("MATCH (a:Person)-[e:KNOWS]->(b:Person) RETURN e.friendship") + val collected = df.collect().map(r => r.getAs[String]("friendship")).toSet + assert(collected === Set("friend", "collegaue", "spoose")) + } + + test("scan-reuse floor: a repeated edge group shares one scan") { + // check that edge scans are reused along the joins + val (_, memo) = runWithMemo("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)") + val knowsScans = memo.iterator.filter(_._1.groupName == "knows").map(_._2).toSeq + assert(knowsScans.length === 1, s"expected one shared KNOWS scan: ${memo.keySet}") + } + + test("scan-reuse floor: differing edge scan filters produce distinct edge scans") { + // Two KNOWS hops. The first edge is filtered (`e1.friendship = 'spouse'`), the second is not, + // so the two KNOWS scans have different ScanKeys -> two distinct KNOWS scans in the memo. + val (_, memo) = runWithMemo( + "MATCH (a:Person)-[e1:KNOWS]->(b:Person)-[e2:KNOWS]->(c:Person) " + + "WHERE e1.friendship = 'spouse'") + val knowsKeys = memo.keySet.filter(_.groupName == "knows").toSeq + assert( + knowsKeys.length === 2, + s"expected two KNOWS scans (filtered vs unfiltered): $knowsKeys") + // And they must be distinct DataFrame references (the whole point of keying on the filter). + val knowsScans = knowsKeys.map(memo) + assert( + knowsScans.head ne knowsScans.last, + "distinct edge signatures must yield distinct scans") + } + + test("undirected pattern over a directed self-loop returns both orientations") { + // KNOWS is directed (1->2, 2->3, 3->1). Undirected must surface each edge BOTH ways. + // No reciprocal pairs exist, so 3 stored edges -> 6 distinct rows. + val df = run("MATCH (a:Person)-[:KNOWS]-(b:Person)") + assert(df.count() === 6) // would be 3 if the backward path were dropped + val rows = df.collect().map(r => (r.getString(0), r.getString(2))).toSet + assert( + rows === Set( + (maskedId(1L, "Person"), maskedId(2L, "Person")), + (maskedId(2L, "Person"), maskedId(3L, "Person")), + (maskedId(3L, "Person"), maskedId(1L, "Person")), + (maskedId(2L, "Person"), maskedId(1L, "Person")), + (maskedId(3L, "Person"), maskedId(2L, "Person")), + (maskedId(1L, "Person"), maskedId(3L, "Person")))) + } + + test("undirected pattern equals the union of both directed arrows") { + // The semantics-pinning invariant: -[:KNOWS]- == (-[:KNOWS]->) ∪ (<-[:KNOWS]-). + def pairs(gql: String) = run(gql).collect().map(r => (r.getString(0), r.getString(2))).toSet + assert( + pairs("MATCH (a:Person)-[:KNOWS]-(b:Person)") === + pairs("MATCH (a:Person)-[:KNOWS]->(b:Person)") ++ pairs( + "MATCH (a:Person)<-[:KNOWS]-(b:Person)")) + } + + test("undirected pattern over a directed cross-group edge matches forward from the src side") { + // WORKS_AT: Person->Company; no Company->Person edge, so only the forward orientation matches. + val df = run("MATCH (a:Person)-[:WORKS_AT]-(c:Company)") + assert(df.count() === 3) + val rows = df.collect().map(r => (r.getString(0), r.getString(2))).toSet + assert( + rows === Set( + (maskedId(1L, "Person"), maskedId(10L, "Company")), + (maskedId(2L, "Person"), maskedId(10L, "Company")), + (maskedId(3L, "Person"), maskedId(20L, "Company")))) + assert(df.head().getAs[String]("start_property_group") === "Person") + assert(df.head().getAs[String]("end_property_group") === "Company") + } + + test("undirected pattern over the same cross-group edge matches backward from the dst side") { + val df = run("MATCH (c:Company)-[:WORKS_AT]-(a:Person)") + assert(df.count() === 3) + val rows = df.collect().map(r => (r.getString(0), r.getString(2))).toSet + assert( + rows === Set( + (maskedId(10L, "Company"), maskedId(1L, "Person")), + (maskedId(10L, "Company"), maskedId(2L, "Person")), + (maskedId(20L, "Company"), maskedId(3L, "Person")))) + assert(df.head().getAs[String]("start_property_group") === "Company") + } + + test("undirected pattern over an UNDIRECTED edge group is not double-counted") { + import sqlImplicits._ + // isDirected=false: getData already emits both orientations, so the resolver must keep a + // SINGLE (forward) path. 2 stored edges -> 4 rows; a regressed dedup would yield 8. + val persons = Seq((1L, "Alice"), (2L, "Bob"), (3L, "Carol")).toDF("id", "name") + val personGroup = VertexPropertyGroup("Person", persons, "id") + val friends = Seq((1L, 2L), (2L, 3L)).toDF("src", "dst") + val friendGroup = EdgePropertyGroup( + "FRIEND", + friends, + personGroup, + personGroup, + isDirected = false, + "src", + "dst", + lit(1.0)) + val ug = PropertyGraphFrame(Seq(personGroup), Seq(friendGroup)) + + val df = runOn(ug, "MATCH (a:Person)-[:FRIEND]-(b:Person)") + assert(df.count() === 4) // NOT 8 -- multiset count is what catches the duplicate + val rows = df.collect().map(r => (r.getString(0), r.getString(2))).toSet + assert( + rows === Set( + (maskedId(1L, "Person"), maskedId(2L, "Person")), + (maskedId(2L, "Person"), maskedId(1L, "Person")), + (maskedId(2L, "Person"), maskedId(3L, "Person")), + (maskedId(3L, "Person"), maskedId(2L, "Person")))) + } + + // --------------------------------------------------------------------------- + // Scalar datetime functions (end-to-end). + // + // These exercise the full pipeline -- parse -> resolve (scan-local / join + // classification through the function-call argument) -> plan -> execute -> + // Spark built-in lowering. A dedicated graph with a DateType property is used + // since the default fixture has no date column. + // --------------------------------------------------------------------------- + private def dateGraph: PropertyGraphFrame = { + import sqlImplicits._ + val persons = Seq( + (1L, "Alice", java.sql.Date.valueOf("2000-01-15")), + (2L, "Bob", java.sql.Date.valueOf("1995-06-20")), + (3L, "Carol", java.sql.Date.valueOf("2000-12-01"))).toDF("id", "name", "birthday") + val personGroup = VertexPropertyGroup("Person", persons, "id") + val knows = Seq((1L, 2L), (2L, 3L), (3L, 1L)).toDF("src", "dst") + val knowsGroup = EdgePropertyGroup( + "KNOWS", + knows, + personGroup, + personGroup, + isDirected = true, + "src", + "dst", + lit(1.0)) + PropertyGraphFrame(Seq(personGroup), Seq(knowsGroup)) + } + + test("year() in a scan-local WHERE filters rows end-to-end") { + // KNOWS: 1(Alice,2000)->2(Bob,1995), 2(Bob,1995)->3(Carol,2000), 3(Carol,2000)->1(Alice,2000). + // `year(a.birthday) = 2000` is scan-local on `a`: Alice(1) and Carol(3) pass. + // As src of KNOWS: Alice->Bob and Carol->Alice => 2 rows. + val df = + runOn(dateGraph, "MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE year(a.birthday) = 2000") + assert(df.count() === 2) + val startIds = df.collect().map(_.getString(0)).toSet + assert(startIds === Set(maskedId(1L, "Person"), maskedId(3L, "Person"))) + } + + test("RETURN year(a.birthday) AS y projects the lowered Spark year() column") { + val df = + runOn(dateGraph, "MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN year(a.birthday) AS y") + assert(df.schema.fieldNames.toSeq === Seq("y")) + val years = df.collect().map(_.getInt(0)).toSet + // src order: Alice(2000), Bob(1995), Carol(2000). + assert(years === Set(2000, 1995)) + } + + test("datediff() in a two-variable WHERE is applied as a join predicate end-to-end") { + // Spark `datediff(endDate, startDate)` = days from startDate to endDate, so + // `datediff(a.birthday, b.birthday)` = days from b's birthday to a's birthday. + // KNOWS edges (a -> b): + // 1->2 Alice(2000-01-15), Bob (1995-06-20): ~1639 days > 300 + + // 2->3 Bob (1995-06-20), Carol(2000-12-01): ~-1636 days < 300 - + // 3->1 Carol(2000-12-01), Alice(2000-01-15): ~321 days > 300 + + // => two edges survive. + val df = + runOn( + dateGraph, + "MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE datediff(a.birthday, b.birthday) > 300") + assert(df.count() === 2) + val startIds = df.collect().map(_.getString(0)).toSet + assert(startIds === Set(maskedId(1L, "Person"), maskedId(3L, "Person"))) + } + + // --------------------------------------------------------------------------- + // Multiplicative operators and scalar functions (end-to-end). + // --------------------------------------------------------------------------- + + test("multiplicative * / % produce correct values end-to-end") { + // a.age is Int. a.age * 2, a.age / 2 (floating-point), a.age % 4 -- project all three. + // Persons: Alice(30), Bob(40), Carol(25). KNOWS: 1->2, 2->3, 3->1. + // NB: Spark widens int*int and int%int to Long; read those columns as Long. + val df = run( + "MATCH (a:Person)-[:KNOWS]->(b:Person) " + + "RETURN a.age * 2 AS dbl, a.age / 2 AS half, a.age % 4 AS mod") + assert(df.schema.fieldNames.toSeq === Seq("dbl", "half", "mod")) + // src order: Alice(30)->Bob, Bob(40)->Carol, Carol(25)->Alice. + val rows = df.collect().map(r => (r.getLong(0), r.getDouble(1), r.getLong(2))).toSet + assert( + rows === Set( + (60L, 15.0, 2L), // Alice 30: 30*2=60, 30/2=15.0, 30%4=2 + (80L, 20.0, 0L), // Bob 40: 40*2=80, 40/2=20.0, 40%4=0 + (50L, 12.5, 1L) + ) + ) // Carol 25: 25*2=50, 25/2=12.5, 25%4=1 + } + + test("WHERE pmod(hash(a.id), N) = 0 returns a deterministic, repeatable subset") { + // The deterministic-sampling idiom: `pmod(hash(a.id), N) = 0` must be reproducible + // across two executions (this is the scan-reuse memo correctness contract -- two identical + // scan signatures must yield identical rows). + val gql = "MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE pmod(hash(a.id), 4) = 0" + val first = run(gql).collect().map(_.getString(0)).toSet + val second = run(gql).collect().map(_.getString(0)).toSet + assert(first === second, "hash-sampling must be deterministic across runs") + // The bucket must be a genuine subset of all source ids (non-empty and not all). + val allSrcs = Set(maskedId(1L, "Person"), maskedId(2L, "Person"), maskedId(3L, "Person")) + assert(first.nonEmpty && first.subsetOf(allSrcs)) + } + + test("lower() string predicate filters rows end-to-end") { + // Need a fixture with a string column to lower-case. KNOWS: 1->2,2->3,3->1. + import sqlImplicits._ + val persons = Seq((1L, "Alice"), (2L, "BOB"), (3L, "carol")).toDF("id", "name") + val personGroup = VertexPropertyGroup("Person", persons, "id") + val knows = Seq((1L, 2L), (2L, 3L), (3L, 1L)).toDF("src", "dst") + val knowsGroup = EdgePropertyGroup( + "KNOWS", + knows, + personGroup, + personGroup, + isDirected = true, + "src", + "dst", + lit(1.0)) + val sg = PropertyGraphFrame(Seq(personGroup), Seq(knowsGroup)) + + // Only Bob('BOB') lower-cases to 'bob'. + val df = runOn(sg, "MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE lower(a.name) = 'bob'") + assert(df.count() === 1) + assert(df.head().getString(0) === maskedId(2L, "Person")) + } + + test("get_json_object over a JSON string property returns expected scalars") { + // A dedicated fixture where every payload is a JSON document; get_json_object takes a + // lit-str path. Persons: Alice(score=42), Bob(score=7), Carol(score=5). + import sqlImplicits._ + val persons = + Seq((1L, """{"score": 42}"""), (2L, """{"score": 7}"""), (3L, """{"score": 5}""")) + .toDF("id", "payload") + val personGroup = VertexPropertyGroup("Person", persons, "id") + val knows = Seq((1L, 2L), (2L, 3L), (3L, 1L)).toDF("src", "dst") + val knowsGroup = EdgePropertyGroup( + "KNOWS", + knows, + personGroup, + personGroup, + isDirected = true, + "src", + "dst", + lit(1.0)) + val jg = PropertyGraphFrame(Seq(personGroup), Seq(knowsGroup)) + + val df = + runOn( + jg, + "MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN get_json_object(a.payload, '$.score') AS s") + val scores = df.collect().map(_.getString(0)).toSet + assert(scores === Set("42", "7", "5")) + } + + test("xpath_int over an XML string property returns expected scalars") { + // A dedicated fixture where every payload is a valid XML document; xpath_int takes a + // Column path. Persons: Alice(10), Bob(20), Carol(99). + // NB: Spark's xpath_* throw on non-XML input rather than returning null, so all rows must be + // well-formed XML. + import sqlImplicits._ + val persons = + Seq((1L, "10"), (2L, "20"), (3L, "99")) + .toDF("id", "payload") + val personGroup = VertexPropertyGroup("Person", persons, "id") + val knows = Seq((1L, 2L), (2L, 3L), (3L, 1L)).toDF("src", "dst") + val knowsGroup = EdgePropertyGroup( + "KNOWS", + knows, + personGroup, + personGroup, + isDirected = true, + "src", + "dst", + lit(1.0)) + val xg = PropertyGraphFrame(Seq(personGroup), Seq(knowsGroup)) + + val df = + runOn(xg, "MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN xpath_int(a.payload, '/a/v') AS s") + val vals = df.collect().map(_.getInt(0)).toSet + assert(vals === Set(10, 20, 99)) + } + + // --- Variable-length patterns (end-to-end) ------------------------------ + // + // KNOWS is a directed 3-cycle: 1(Alice) -> 2(Bob) -> 3(Carol) -> 1(Alice). That makes the + // walks of each length fully enumerable by hand, so these assert exact rows, exact intermediate + // ids in the `path` array, and structural properties (cycle closure) -- not just counts. + // Range forms (*lo..hi) are used so these exercise the executor regardless of the AstBuilder + // exact-form (*N) handling. + + private def P(id: Long): String = maskedId(id, "Person") + + private def pathArray( + r: org.apache.spark.sql.Row): scala.collection.Seq[org.apache.spark.sql.Row] = + r.get(r.fieldIndex("path")).asInstanceOf[scala.collection.Seq[org.apache.spark.sql.Row]] + + test("var-length *1..2 unions the 1-hop and 2-hop walks with correct path arrays") { + // 1-hop: (1,2),(2,3),(3,1). 2-hop: (1,3),(2,1),(3,2). + val df = run("MATCH (a:Person)-[:KNOWS*1..2]->(b:Person)") + val rows = df.collect() + assert(rows.length === 6) + + val pairs = rows.map(r => (r.getAs[String]("start_id"), r.getAs[String]("end_id"))).toSet + assert( + pairs === Set( + (P(1), P(2)), + (P(2), P(3)), + (P(3), P(1)), // length 1 + (P(1), P(3)), + (P(2), P(1)), + (P(3), P(2)) + ) + ) // length 2 + + // path-array length == number of hops: 1 for the direct walks, 2 for the two-hop walks. + val sizeByPair = + rows + .map(r => (r.getAs[String]("start_id"), r.getAs[String]("end_id")) -> pathArray(r).length) + .toMap + assert(sizeByPair((P(1), P(2))) === 1) + assert(sizeByPair((P(1), P(3))) === 2) + + // The 1->2->3 two-hop row must carry the intermediate node (Bob=2) in path[0]. + val twoHop = + rows.find(r => r.getAs[String]("start_id") == P(1) && r.getAs[String]("end_id") == P(3)).get + val path = pathArray(twoHop) + assert(path.length === 2) + assert(path(0).getAs[String](0) === "KNOWS") // edge_property_group + assert(path(0).getAs[String](1) === P(2)) // node_id (the intermediate, Bob) + assert(path(0).getAs[String](2) === "Person") // node_property_group + assert(path(1).getAs[String](0) === "KNOWS") // final hop: edge only... + assert(path(1).isNullAt(1)) // ...node fields null (end node is in end_id) + } + + test("var-length *2..2 yields exactly the two-hop walks with the right intermediate id") { + // 1->2->3, 2->3->1, 3->1->2. + val df = run("MATCH (a:Person)-[:KNOWS*2..2]->(b:Person)") + val rows = df.collect() + assert(rows.length === 3) + assert(rows.forall(r => pathArray(r).length == 2)) + + val byStart = rows.map(r => r.getAs[String]("start_id") -> r).toMap + def endOf(r: org.apache.spark.sql.Row) = r.getAs[String]("end_id") + def midOf(r: org.apache.spark.sql.Row) = pathArray(r)(0).getAs[String](1) + + assert(endOf(byStart(P(1))) === P(3) && midOf(byStart(P(1))) === P(2)) // 1->2->3 + assert(endOf(byStart(P(2))) === P(1) && midOf(byStart(P(2))) === P(3)) // 2->3->1 + assert(endOf(byStart(P(3))) === P(2) && midOf(byStart(P(3))) === P(1)) // 3->1->2 + } + + test("var-length *3..3 traverses the full cycle: start equals end") { + // Each 3-hop walk returns to its start (the cycle has length 3). + val df = run("MATCH (a:Person)-[:KNOWS*3..3]->(b:Person)") + val rows = df.collect() + assert(rows.length === 3) + assert(rows.forall(r => r.getAs[String]("start_id") == r.getAs[String]("end_id"))) + assert(rows.map(_.getAs[String]("start_id")).toSet === Set(P(1), P(2), P(3))) + + // 1->2->3->1 carries intermediates Bob(2) then Carol(3); last entry has null node fields. + val r1 = rows.find(_.getAs[String]("start_id") == P(1)).get + val path = pathArray(r1) + assert(path.length === 3) + assert(path(0).getAs[String](1) === P(2)) + assert(path(1).getAs[String](1) === P(3)) + assert(path(2).isNullAt(1)) + } + + test("scan-local WHERE prunes the var-length start node end-to-end") { + // Only Bob (age 40) passes age > 30. From Bob: 2->3 (len 1) and 2->3->1 (len 2). + val df = run("MATCH (a:Person)-[:KNOWS*1..2]->(b:Person) WHERE a.age > 30") + val rows = df.collect() + assert(rows.length === 2) + assert(rows.forall(_.getAs[String]("start_id") == P(2))) + assert(rows.map(_.getAs[String]("end_id")).toSet === Set(P(3), P(1))) + } + + test("var-length spliced before a fixed hop produces a heterogeneous path") { + // (a)-[:KNOWS*1..2]->(b)-[:WORKS_AT]->(c): 3 one-knows + 3 two-knows = 6 rows, ending on Company. + val df = run("MATCH (a:Person)-[:KNOWS*1..2]->(b:Person)-[:WORKS_AT]->(c:Company)") + val rows = df.collect() + assert(rows.length === 6) + assert(rows.forall(_.getAs[String]("start_property_group") == "Person")) + assert(rows.forall(_.getAs[String]("end_property_group") == "Company")) + + // path length = total hops: 2 for one-KNOWS rows, 3 for two-KNOWS rows. + val sizes = + rows.map(r => pathArray(r).length).groupBy(identity).map(f => f._1 -> f._2.size).toMap + assert(sizes === Map(2 -> 3, 3 -> 3)) + // The final hop is always WORKS_AT (the fixed tail), regardless of the KNOWS length. + assert(rows.forall(r => pathArray(r).last.getAs[String](0) == "WORKS_AT")) + } +} diff --git a/core/src/test/scala/org/graphframes/propertygraph/internal/ResolverCaseInsensitiveSuite.scala b/core/src/test/scala/org/graphframes/propertygraph/internal/ResolverCaseInsensitiveSuite.scala new file mode 100644 index 000000000..b4b9b1244 --- /dev/null +++ b/core/src/test/scala/org/graphframes/propertygraph/internal/ResolverCaseInsensitiveSuite.scala @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +import org.graphframes.InvalidPropertyGroupException +import org.graphframes.SparkFunSuite +import org.graphframes.propertygraph.QueryOptions + +/** + * Regression tests that lock in case-insensitive matching of vertex and edge labels in the GQL + * resolver. A query like `MATCH (a:person)` must resolve against a schema registered as `Person`, + * and the resolved [[PathNode]]s must carry the canonical-case names from the schema (not the + * casing the user typed). + */ +class ResolverCaseInsensitiveSuite extends SparkFunSuite { + + // Schema names use distinctive casing so tests can distinguish query casing from canonical. + private val schema: SchemaGraphSnapshot = SchemaGraphSnapshot( + vertexGroupNames = Set("Person", "Company"), + edges = Vector( + SchemaEdge("KNOWS", "Person", "Person", true), + SchemaEdge("WORKS_AT", "Person", "Company", true))) + + val options: QueryOptions = QueryOptions() + + // ----- vertex label resolution -------------------------------------------- + + test("lowercase vertex label resolves and preserves canonical case") { + val ast = AstBuilder.parse("MATCH (a:person)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.length === 1) + assert(rq.paths.head.nodes.head.vertexGroupName === "Person") + } + + test("uppercase vertex label resolves and preserves canonical case") { + val ast = AstBuilder.parse("MATCH (a:PERSON)-[:WORKS_AT]->(b:company)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.length === 1) + assert(rq.paths.head.nodes.map(_.vertexGroupName) === Vector("Person", "Company")) + } + + test("mixed-case vertex label resolves and preserves canonical case") { + val ast = AstBuilder.parse("MATCH (a:PeRsOn)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.length === 1) + assert(rq.paths.head.nodes.head.vertexGroupName === "Person") + } + + // ----- edge label resolution ---------------------------------------------- + + test("lowercase edge label resolves and preserves canonical case") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:knows]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.length === 1) + assert(rq.paths.head.steps.head.edge.edgeGroupName === "KNOWS") + } + + test("mixed-case edge label resolves and preserves canonical case") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:WoRkS_aT]->(b:Company)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.length === 1) + assert(rq.paths.head.steps.head.edge.edgeGroupName === "WORKS_AT") + } + + // ----- combined node + edge case-insensitivity ---------------------------- + + test("all-uppercase query resolves against mixed-case schema") { + val ast = AstBuilder.parse("MATCH (a:PERSON)-[:WORKS_AT]->(b:COMPANY)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.length === 1) + assert(rq.paths.head.nodes.map(_.vertexGroupName) === Vector("Person", "Company")) + assert(rq.paths.head.steps.head.edge.edgeGroupName === "WORKS_AT") + } + + // ----- error path unchanged ----------------------------------------------- + + test("unknown vertex label still throws InvalidPropertyGroupException") { + val ast = AstBuilder.parse("MATCH (a:Nonexistent)") + intercept[InvalidPropertyGroupException] { + Resolver.resolve(ast, schema, options) + } + } + + test("unknown edge label still throws InvalidPropertyGroupException") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:HATES]->(b:Person)") + intercept[InvalidPropertyGroupException] { + Resolver.resolve(ast, schema, options) + } + } +} diff --git a/core/src/test/scala/org/graphframes/propertygraph/internal/ResolverSuite.scala b/core/src/test/scala/org/graphframes/propertygraph/internal/ResolverSuite.scala new file mode 100644 index 000000000..cdde12e22 --- /dev/null +++ b/core/src/test/scala/org/graphframes/propertygraph/internal/ResolverSuite.scala @@ -0,0 +1,873 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.graphframes.propertygraph.internal + +import org.graphframes.InvalidPropertyGroupException +import org.graphframes.SparkFunSuite +import org.graphframes.propertygraph.QueryOptions + +/** + * Pure-JVM tests for `Resolver.resolve`. Each test builds a `MatchStatement` AST by hand (or via + * `AstBuilder.parse`) plus a small `SchemaGraphSnapshot`, and asserts on the resulting + * `ResolvedQuery`. No SparkSession required — resolution is pure JVM. + */ +class ResolverSuite extends SparkFunSuite { + + // A small schema used across several tests: + // Person --KNOWS--> Person + // Person --WORKS_AT--> Company + // Company --LOCATED_IN--> City + private val schema = SchemaGraphSnapshot( + vertexGroupNames = Set("Person", "Company", "City"), + edges = Vector( + SchemaEdge("KNOWS", "Person", "Person", true), + SchemaEdge("WORKS_AT", "Person", "Company", true), + SchemaEdge("LOCATED_IN", "Company", "City", true))) + + private val options = QueryOptions() + + test("typed single-hop pattern resolves to exactly one path") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.paths.length === 1) + val path = rq.paths.head + assert(path.length === 1) + assert(path.nodes.map(_.vertexGroupName) === Vector("Person", "Person")) + assert(path.steps.head.edge === SchemaEdge("KNOWS", "Person", "Person", true)) + assert(path.steps.head.traversedForward === true) + assert(path.nodes.head.variable === Some("a")) + assert(path.nodes(1).variable === Some("b")) + assert(rq.projection === Projection.Default) + } + + test("untyped middle node fans out over reachable schema paths") { + // MATCH (a:Person)-[]->(x)-[]->(b:City): from Person, the only 2-hop chain landing on City is + // Person -WORKS_AT-> Company -LOCATED_IN-> City. (Person-KNOWS->Person has no onward edge to + // City.) So exactly one path survives, with the middle node resolved to Company. + val ast = AstBuilder.parse("MATCH (a:Person)-[]->(x)-[]->(b:City)") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.paths.length === 1) + val path = rq.paths.head + assert(path.nodes.map(_.vertexGroupName) === Vector("Person", "Company", "City")) + assert(path.steps(0).edge === SchemaEdge("WORKS_AT", "Person", "Company", true)) + assert(path.steps(1).edge === SchemaEdge("LOCATED_IN", "Company", "City", true)) + assert(path.steps.forall(_.traversedForward === true)) + } + + test("disconnected pattern yields no paths") { + // No schema edge has City as src and Person as dst, and no 2-hop City->...->Person exists. + val ast = AstBuilder.parse("MATCH (a:City)-[:KNOWS]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.isEmpty) + } + + test("right-to-left arrow produces a backward step") { + val ast = AstBuilder.parse("MATCH (a:Company)<-[:WORKS_AT]-(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.paths.length === 1) + val step = rq.paths.head.steps.head + // The edge group is WORKS_AT: Person->Company. Traversed right-to-left means the current node + // is the edge's dst (Company) and the next node is the edge's src (Person). + assert(step.edge === SchemaEdge("WORKS_AT", "Person", "Company", true)) + assert(step.traversedForward === false) + assert(rq.paths.head.nodes.map(_.vertexGroupName) === Vector("Company", "Person")) + } + + test("self-loop group is enumerated without special-casing") { + // KNOWS: Person->Person is a self-loop. A typed pattern over it resolves to a single path. + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.paths.length === 1) + assert(rq.paths.head.length === 2) + assert(rq.paths.head.steps.forall(_.traversedForward === true)) + } + + test("untyped source node fans out over all vertex groups") { + // MATCH (x)-[:LOCATED_IN]->(b:City): x must be a group that has an outgoing LOCATED_IN edge, + // i.e. Company only. So one path. + val ast = AstBuilder.parse("MATCH (x)-[:LOCATED_IN]->(b:City)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.length === 1) + assert(rq.paths.head.nodes.head.vertexGroupName === "Company") + } + + test("untyped edge fans out over all candidate edge groups") { + // MATCH (a:Person)-[]->(b:Person): Person->Person edges are KNOWS only. One path. + // Person-WORKS_AT->Company does not land on Person. So one path. + val ast = AstBuilder.parse("MATCH (a:Person)-[]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.length === 1) + assert(rq.paths.head.steps.head.edge.edgeGroupName === "KNOWS") + } + + test("unknown vertex label throws InvalidPropertyGroupException") { + val ast = AstBuilder.parse("MATCH (a:Walrus)") + intercept[InvalidPropertyGroupException] { + Resolver.resolve(ast, schema, options) + } + } + + test("unknown edge label throws InvalidPropertyGroupException") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:HATES]->(b:Person)") + intercept[InvalidPropertyGroupException] { + Resolver.resolve(ast, schema, options) + } + } + + test("scan-local WHERE predicate is attached to the matching PathNode") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > 30") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.joinPredicates === Nil) + assert(rq.postFilters === Nil) + val nodeA = rq.paths.head.nodes.head + val nodeB = rq.paths.head.nodes(1) + assert(nodeA.scanFilter.length === 1) + assert(nodeA.scanFilter.head === Comparison(PropertyAccess("a", "age"), Gt, Literal(30L))) + assert(nodeB.scanFilter === Nil) + } + + test("two-variable adjacent WHERE predicate becomes a join predicate") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > b.age") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.joinPredicates.length === 1) + assert( + rq.joinPredicates.head === Comparison( + PropertyAccess("a", "age"), + Gt, + PropertyAccess("b", "age"))) + assert(rq.paths.head.nodes.forall(_.scanFilter === Nil)) + assert(rq.postFilters === Nil) + } + + test("three-variable WHERE predicate becomes a post-filter") { + val ast = AstBuilder.parse( + "MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) WHERE a.age > c.age") + val rq = Resolver.resolve(ast, schema, options) + + // a and c are non-adjacent (positions 0 and 4, differ by 4, not 2). + assert(rq.joinPredicates === Nil) + assert(rq.postFilters.length === 1) + assert( + rq.postFilters.head === Comparison( + PropertyAccess("a", "age"), + Gt, + PropertyAccess("c", "age"))) + } + + test("AND is split so each conjunct is classified independently") { + val ast = AstBuilder.parse( + "MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.age > 30 AND a.age > b.age AND 1 = 1") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.paths.head.nodes.head.scanFilter.length === 1) // a.age > 30 + assert(rq.joinPredicates.length === 1) // a.age > b.age + // The literal-only `1 = 1` references no node variable and falls into post-filters. + assert(rq.postFilters.length === 1) + } + + // ----------------------------------------------------------------------- + // Scalar function calls in WHERE: classification regression guards. + // These guard the §4 traversal edits (`referencedVariables` + `propertyAccesses`): + // a function call over a property must still contribute its variable so the + // resolver can classify the predicate (scan-local vs join vs post). + // ----------------------------------------------------------------------- + test("scan-local WHERE predicate inside a function call is attached to the matching node") { + // `year(a.creationDate) = 2012` references only `a` -> scan-local on `a`. + val ast = + AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE year(a.creationDate) = 2012") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.joinPredicates === Nil) + assert(rq.postFilters === Nil) + val nodeA = rq.paths.head.nodes.head + val nodeB = rq.paths.head.nodes(1) + assert(nodeA.scanFilter.length === 1) + assert( + nodeA.scanFilter.head === Comparison( + FunctionCall("year", Seq(PropertyAccess("a", "creationDate"))), + Eq, + Literal(2012L))) + assert(nodeB.scanFilter === Nil) + } + + test("two-variable adjacent WHERE predicate inside a function call becomes a join predicate") { + // `datediff(a.d, b.d) > 30` references `a` and `b` (adjacent) -> join predicate, with both + // `a.d` and `b.d` carried (this is the silent-drop regression guard: if `propertyAccesses` + // forgot the FunctionCall arm, these columns would not be classified as carry-to-scan). + val ast = + AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE datediff(a.d, b.d) > 30") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.joinPredicates.length === 1) + assert( + rq.joinPredicates.head === Comparison( + FunctionCall("datediff", Seq(PropertyAccess("a", "d"), PropertyAccess("b", "d"))), + Gt, + Literal(30L))) + assert(rq.paths.head.nodes.forall(_.scanFilter === Nil)) + assert(rq.postFilters === Nil) + } + + test( + "hash-sampling WHERE predicate (pmod + hash) is classified scan-local on its only variable") { + // `pmod(hash(a.id), 512) = 0` references only `a` -> scan-local on `a`. This is the + // deterministic-sampling pushdown guard (spec §3): the predicate must reach the scan so the + // ScanKey memo sees a reproducible filter, not a post-join filter. + val ast = + AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE pmod(hash(a.id), 512) = 0") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.joinPredicates === Nil) + assert(rq.postFilters === Nil) + val nodeA = rq.paths.head.nodes.head + val nodeB = rq.paths.head.nodes(1) + assert(nodeA.scanFilter.length === 1) + assert( + nodeA.scanFilter.head === Comparison( + FunctionCall( + "pmod", + Seq(FunctionCall("hash", Seq(PropertyAccess("a", "id"))), Literal(512L))), + Eq, + Literal(0L))) + assert(nodeB.scanFilter === Nil) + } + + test("multiplicative cross-variable WHERE predicate becomes a join predicate") { + // `a.x * 2 > b.y` references `a` and `b` (adjacent) -> join predicate. Regression guard for + // the widened ArithOp: the Arithmetic node must still contribute both variables through + // referencedVariables / propertyAccesses (the node match is unchanged, only the op type + // widened, but this pins the behaviour). + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.x * 2 > b.y") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.joinPredicates.length === 1) + assert( + rq.joinPredicates.head === Comparison( + Arithmetic(PropertyAccess("a", "x"), Mult, Literal(2L)), + Gt, + PropertyAccess("b", "y"))) + assert(rq.paths.head.nodes.forall(_.scanFilter === Nil)) + assert(rq.postFilters === Nil) + } + + test("projection default when RETURN omitted") { + val ast = AstBuilder.parse("MATCH (a:Person)") + assert(Resolver.resolve(ast, schema, options).projection === Projection.Default) + } + + test("projection star") { + val ast = AstBuilder.parse("MATCH (a:Person) RETURN *") + assert(Resolver.resolve(ast, schema, options).projection === Projection.Star) + } + + test("projection items") { + val ast = AstBuilder.parse("MATCH (a:Person) RETURN a, a.name AS n") + val Projection.Items(items) = Resolver.resolve(ast, schema, options).projection + assert(items.length === 2) + assert(items(1).alias === Some("n")) + } + + test("fan-out produces multiple paths for a fully untyped 2-hop pattern") { + // A dedicated schema with multiple 2-hop chains so fan-out is observable. + val multi = SchemaGraphSnapshot( + vertexGroupNames = Set("A", "B", "C", "D"), + edges = Vector( + SchemaEdge("e1", "A", "B", true), + SchemaEdge("e2", "A", "C", true), + SchemaEdge("e3", "B", "D", true), + SchemaEdge("e4", "C", "D", true))) + val ast = AstBuilder.parse("MATCH (x:A)-[]->()-[]->(y:D)") + val rq = Resolver.resolve(ast, multi, options) + + // Two chains: A-e1->B-e3->D and A-e2->C-e4->D. + assert(rq.paths.length === 2) + val midGroups = rq.paths.map(_.nodes(1).vertexGroupName).toSet + assert(midGroups === Set("B", "C")) + rq.paths.foreach { p => + assert(p.steps.forall(_.traversedForward === true)) + assert(p.nodes.head.vertexGroupName === "A") + assert(p.nodes.last.vertexGroupName === "D") + } + } + + // ========================================================================= + // Direct tests for `Resolver.enumeratePaths`. + // + // `enumeratePaths` is the bounded DFS that turns a linear node/edge pattern + // plus a schema snapshot into 0..N concrete `SchemaPath`s. These tests call + // it directly (bypassing label validation, WHERE classification, projection) + // so each DFS branch and corner case is covered in isolation. Note that, + // unlike `resolve`, `enumeratePaths` does NOT throw on unknown labels: it + // simply yields no paths (the throw happens upstream in `validateLabels`). + // ========================================================================= + + /** Extract (nodes, edges) in user order from a parsed GQL pattern. */ + private def parsed(query: String): (Seq[NodePattern], Seq[EdgePattern]) = { + val ast = AstBuilder.parse(query) + val nodes = ast.pattern.elements.collect { case n: NodePattern => n } + val edges = ast.pattern.elements.collect { case e: EdgePattern => e } + (nodes, edges) + } + + // --- Single-node patterns (zero edges / DFS leaf at root) --------------- + + test("enumeratePaths: typed single node yields one zero-step path") { + val (nodes, edges) = parsed("MATCH (a:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + val p = paths.head + assert(p.length === 0) + assert(p.steps.isEmpty) + assert(p.nodes.length === 1) + assert(p.nodes.head.vertexGroupName === "Person") + assert(p.nodes.head.variable === Some("a")) + assert(p.nodes.head.scanFilter === Nil) + } + + test("enumeratePaths: untyped single node fans out over every vertex group") { + val (nodes, edges) = parsed("MATCH (x)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 3) + assert(paths.map(_.nodes.head.vertexGroupName).toSet === Set("Person", "Company", "City")) + paths.foreach { p => + assert(p.length === 0) + assert(p.steps.isEmpty) + assert(p.nodes.head.variable === Some("x")) + } + } + + test("enumeratePaths: anonymous single node fans out with no variable") { + val (nodes, edges) = parsed("MATCH ()") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 3) + paths.foreach { p => + assert(p.length === 0) + assert(p.nodes.head.variable === None) + } + } + + test("enumeratePaths: label-only single node (no variable) resolves") { + val (nodes, edges) = parsed("MATCH (:Company)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + assert(paths.head.nodes.head.vertexGroupName === "Company") + assert(paths.head.nodes.head.variable === None) + } + + test("enumeratePaths: typed start label matches case-insensitively") { + val (nodes, edges) = parsed("MATCH (a:PERSON)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + // The resolved group preserves the schema's canonical casing, not the query's casing. + assert(paths.head.nodes.head.vertexGroupName === "Person") + } + + test("enumeratePaths: unknown start label yields no paths rather than throwing") { + // `validateLabels` (upstream) is what throws; the DFS itself just sees an empty start set. + val (nodes, edges) = parsed("MATCH (a:Walrus)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.isEmpty) + } + + test("enumeratePaths: empty schema yields no paths for an untyped single node") { + val empty = SchemaGraphSnapshot(vertexGroupNames = Set.empty, edges = Vector.empty) + val (nodes, edges) = parsed("MATCH (x)") + val paths = Resolver.enumeratePaths(nodes, edges, empty, options) + assert(paths.isEmpty) + } + + // --- Single-hop forward ------------------------------------------------- + + test("enumeratePaths: typed forward single hop resolves to exactly one path") { + val (nodes, edges) = parsed("MATCH (a:Person)-[:KNOWS]->(b:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + val p = paths.head + assert(p.length === 1) + assert(p.steps.head.edge === SchemaEdge("KNOWS", "Person", "Person", true)) + assert(p.steps.head.traversedForward === true) + assert(p.nodes.map(_.vertexGroupName) === Vector("Person", "Person")) + assert(p.nodes.map(_.variable) === Vector(Some("a"), Some("b"))) + } + + test("enumeratePaths: untyped edge from Person fans out over both outgoing groups") { + // Person has two outgoing edges: KNOWS->Person and WORKS_AT->Company. + val (nodes, edges) = parsed("MATCH (a:Person)-[]->(b)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 2) + val edgeNames = paths.map(_.steps.head.edge.edgeGroupName).toSet + assert(edgeNames === Set("KNOWS", "WORKS_AT")) + paths.foreach { p => + assert(p.steps.head.traversedForward === true) + assert(p.nodes.head.vertexGroupName === "Person") + } + } + + test("enumeratePaths: typed edge label prunes unlabelled candidates") { + // From Person the candidates are KNOWS->Person and WORKS_AT->Company; typing the edge as + // WORKS_AT keeps only the latter. + val (nodes, edges) = parsed("MATCH (a:Person)-[:WORKS_AT]->(b)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + assert(paths.head.steps.head.edge.edgeGroupName === "WORKS_AT") + assert(paths.head.nodes.map(_.vertexGroupName) === Vector("Person", "Company")) + } + + test("enumeratePaths: typed next-node label prunes candidates") { + // From Person (untyped edge), only WORKS_AT lands on Company. + val (nodes, edges) = parsed("MATCH (a:Person)-[]->(b:Company)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + assert(paths.head.steps.head.edge.edgeGroupName === "WORKS_AT") + assert(paths.head.nodes.last.vertexGroupName === "Company") + } + + test("enumeratePaths: edge label absent from outgoing candidates yields no paths") { + // Company's only outgoing edge is LOCATED_IN->City; asking for KNOWS yields nothing. + val (nodes, edges) = parsed("MATCH (a:Company)-[:KNOWS]->(b)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.isEmpty) + } + + test("enumeratePaths: start group with no outgoing edges yields no forward paths") { + // City is a pure sink in the schema. + val (nodes, edges) = parsed("MATCH (a:City)-[]->(b)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.isEmpty) + } + + test("enumeratePaths: next-node label that no candidate satisfies yields no paths") { + // From Person, neither KNOWS->Person nor WORKS_AT->Company lands on City. + val (nodes, edges) = parsed("MATCH (a:Person)-[]->(b:City)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.isEmpty) + } + + test("enumeratePaths: edge label matches case-insensitively") { + val (nodes, edges) = parsed("MATCH (a:Person)-[:knows]->(b:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + // The step carries the schema's canonical edge-group name. + assert(paths.head.steps.head.edge.edgeGroupName === "KNOWS") + } + + test("enumeratePaths: next-node label matches case-insensitively") { + val (nodes, edges) = parsed("MATCH (a:Person)-[]->(b:COMPANY)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + assert(paths.head.nodes.last.vertexGroupName === "Company") + } + + test("enumeratePaths: edge variable is preserved on the resolved step") { + val (nodes, edges) = parsed("MATCH (a:Person)-[e:KNOWS]->(b:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + assert(paths.head.steps.head.variable === Some("e")) + } + + // --- Single-hop backward (right-to-left arrow) -------------------------- + + test("enumeratePaths: typed backward single hop produces one backward step") { + val (nodes, edges) = parsed("MATCH (a:Company)<-[:WORKS_AT]-(b:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + val p = paths.head + assert(p.length === 1) + assert(p.steps.head.edge === SchemaEdge("WORKS_AT", "Person", "Company", true)) + assert(p.steps.head.traversedForward === false) + // Backward step: current node is the edge's dst (Company), next node is the edge's src (Person). + assert(p.nodes.map(_.vertexGroupName) === Vector("Company", "Person")) + } + + test("enumeratePaths: untyped backward edge fans out over all incoming edges") { + // Untyped start + backward edge: each start group enumerates its incoming edges. + val (nodes, edges) = parsed("MATCH (x)<-[]-(y)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + // Start candidates: Person, Company, City. + // - Person: incoming = KNOWS from Person. => Person<-KNOWS-Person. + // - Company: incoming = WORKS_AT from Person. => Company<-WORKS_AT-Person. + // - City: incoming = LOCATED_IN from Company. => City<-LOCATED_IN-Company. + assert(paths.length === 3) + paths.foreach { p => + assert(p.length === 1) + assert(p.steps.head.traversedForward === false) + // For a backward step, the next node (y) is the edge's src. + assert(p.nodes.head.vertexGroupName === p.steps.head.edge.dstVertexGroupName) + assert(p.nodes.last.vertexGroupName === p.steps.head.edge.srcVertexGroupName) + } + val edgeNames = paths.map(_.steps.head.edge.edgeGroupName).toSet + assert(edgeNames === Set("KNOWS", "WORKS_AT", "LOCATED_IN")) + } + + test("enumeratePaths: sink group has no incoming edges and yields no backward paths") { + // Person has no incoming-from-outside... actually Person has KNOWS from Person. Use a group + // with no incoming at all. In this schema every group has an incoming edge, so build a + // dedicated one. + val src = SchemaGraphSnapshot( + vertexGroupNames = Set("A", "B"), + edges = Vector(SchemaEdge("ab", "A", "B", true))) + val (nodes, edges) = parsed("MATCH (x:A)<-[]-(y)") + val paths = Resolver.enumeratePaths(nodes, edges, src, options) + assert(paths.isEmpty) + } + + // --- Multi-hop patterns ------------------------------------------------- + + test("enumeratePaths: long linear chain resolves to exactly one path") { + val chain = SchemaGraphSnapshot( + vertexGroupNames = Set("A", "B", "C", "D"), + edges = Vector( + SchemaEdge("e1", "A", "B", true), + SchemaEdge("e2", "B", "C", true), + SchemaEdge("e3", "C", "D", true))) + val (nodes, edges) = parsed("MATCH (x:A)-[]->()-[]->()-[]->(y:D)") + val paths = Resolver.enumeratePaths(nodes, edges, chain, options) + assert(paths.length === 1) + val p = paths.head + assert(p.length === 3) + assert(p.nodes.map(_.vertexGroupName) === Vector("A", "B", "C", "D")) + assert(p.steps.map(_.edge.edgeGroupName) === Vector("e1", "e2", "e3")) + assert(p.steps.forall(_.traversedForward === true)) + } + + test("enumeratePaths: diamond schema fans out into two reconverging paths") { + val diamond = SchemaGraphSnapshot( + vertexGroupNames = Set("A", "B", "C", "D"), + edges = Vector( + SchemaEdge("ab", "A", "B", true), + SchemaEdge("ac", "A", "C", true), + SchemaEdge("bd", "B", "D", true), + SchemaEdge("cd", "C", "D", true))) + val (nodes, edges) = parsed("MATCH (x:A)-[]->()-[]->(y:D)") + val paths = Resolver.enumeratePaths(nodes, edges, diamond, options) + assert(paths.length === 2) + val midGroups = paths.map(_.nodes(1).vertexGroupName).toSet + assert(midGroups === Set("B", "C")) + paths.foreach { p => + assert(p.length === 2) + assert(p.nodes.head.vertexGroupName === "A") + assert(p.nodes.last.vertexGroupName === "D") + assert(p.steps.forall(_.traversedForward === true)) + } + } + + test("enumeratePaths: mid-path dead-end prunes an otherwise viable first hop") { + // Pattern: Person -[]-> () -[]-> Person. + // Hop-1 candidates from Person: KNOWS->Person, WORKS_AT->Company. + // - KNOWS->Person: from Person, KNOWS->Person survives the Person filter; WORKS_AT->Company + // is pruned. => one path Person-KNOWS->Person-KNOWS->Person. + // - WORKS_AT->Company: from Company, only LOCATED_IN->City, which fails the Person filter. + // => no paths. + // Net: exactly one path survives, demonstrating the DFS prunes mid-chain, not just at the root. + val (nodes, edges) = parsed("MATCH (a:Person)-[]->()-[]->(b:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + val p = paths.head + assert(p.length === 2) + assert(p.nodes.map(_.vertexGroupName) === Vector("Person", "Person", "Person")) + assert(p.steps.map(_.edge.edgeGroupName) === Vector("KNOWS", "KNOWS")) + } + + test("enumeratePaths: self-loop group enumerates a multi-hop chain without special-casing") { + val (nodes, edges) = parsed("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + val p = paths.head + assert(p.length === 2) + assert(p.steps.forall(_.edge.edgeGroupName === "KNOWS")) + assert(p.steps.forall(_.traversedForward === true)) + assert(p.nodes.map(_.vertexGroupName) === Vector("Person", "Person", "Person")) + assert(p.nodes.map(_.variable) === Vector(Some("a"), Some("b"), Some("c"))) + } + + test("enumeratePaths: mixed forward and backward arrows in one pattern") { + // Two employees of the same company: Person -WORKS_AT-> Company <-WORKS_AT- Person. + val (nodes, edges) = + parsed("MATCH (a:Person)-[:WORKS_AT]->(c:Company)<-[:WORKS_AT]-(b:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, schema, options) + assert(paths.length === 1) + val p = paths.head + assert(p.length === 2) + assert(p.steps.map(_.traversedForward) === Vector(true, false)) + assert(p.steps.map(_.edge.edgeGroupName) === Vector("WORKS_AT", "WORKS_AT")) + assert(p.nodes.map(_.vertexGroupName) === Vector("Person", "Company", "Person")) + } + + // --- Parallel edges & exhaustive fan-out -------------------------------- + + test("enumeratePaths: parallel edge groups between the same vertex pair each yield a path") { + val parallel = SchemaGraphSnapshot( + vertexGroupNames = Set("A", "B"), + edges = Vector(SchemaEdge("e1", "A", "B", true), SchemaEdge("e2", "A", "B", true))) + val (nodes, edges) = parsed("MATCH (a:A)-[]->(b:B)") + val paths = Resolver.enumeratePaths(nodes, edges, parallel, options) + assert(paths.length === 2) + assert(paths.map(_.steps.head.edge.edgeGroupName).toSet === Set("e1", "e2")) + paths.foreach { p => + assert(p.nodes.map(_.vertexGroupName) === Vector("A", "B")) + } + } + + test("enumeratePaths: fully untyped multi-hop enumerates every reachable chain") { + // On the diamond schema, only A has 2-hop chains (A->B->D, A->C->D); B/C/D are dead-ends + // after one hop. So even with every slot untyped, only 2 paths survive. + val diamond = SchemaGraphSnapshot( + vertexGroupNames = Set("A", "B", "C", "D"), + edges = Vector( + SchemaEdge("ab", "A", "B", true), + SchemaEdge("ac", "A", "C", true), + SchemaEdge("bd", "B", "D", true), + SchemaEdge("cd", "C", "D", true))) + val (nodes, edges) = parsed("MATCH (x)-[]->()-[]->(y)") + val paths = Resolver.enumeratePaths(nodes, edges, diamond, options) + assert(paths.length === 2) + paths.foreach { p => + assert(p.length === 2) + assert(p.nodes.head.vertexGroupName === "A") + assert(p.nodes.last.vertexGroupName === "D") + } + } + + test("enumeratePaths: edge-less schema yields no paths for any multi-hop pattern") { + val noEdges = SchemaGraphSnapshot(vertexGroupNames = Set("A", "B"), edges = Vector.empty) + val (nodes, edges) = parsed("MATCH (a:A)-[]->(b:B)") + val paths = Resolver.enumeratePaths(nodes, edges, noEdges, options) + assert(paths.isEmpty) + } + + // --- Undirected patterns ------------------------------------------------- + // + // KNOWS: Person -> Person, UNDIRECTED (isDirected = false) + // FOLLOWS: Person -> Person, directed + // WORKS_AT: Person -> Company, directed + private val mixedSchema = SchemaGraphSnapshot( + vertexGroupNames = Set("Person", "Company"), + edges = Vector( + SchemaEdge("KNOWS", "Person", "Person", isDirected = false), + SchemaEdge("FOLLOWS", "Person", "Person", isDirected = true), + SchemaEdge("WORKS_AT", "Person", "Company", isDirected = true))) + + test( + "enumeratePaths: undirected pattern over a directed cross-group edge -> one forward path") { + val (nodes, edges) = parsed("MATCH (a:Person)-[:WORKS_AT]-(b:Company)") + val paths = Resolver.enumeratePaths(nodes, edges, mixedSchema, options) + assert(paths.length === 1) + assert(paths.head.steps.head.edge.edgeGroupName === "WORKS_AT") + assert(paths.head.steps.head.traversedForward === true) + assert(paths.head.nodes.map(_.vertexGroupName) === Vector("Person", "Company")) + } + + test( + "enumeratePaths: undirected pattern over a directed edge from the dst side -> one backward path") { + val (nodes, edges) = parsed("MATCH (a:Company)-[:WORKS_AT]-(b:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, mixedSchema, options) + assert(paths.length === 1) + assert(paths.head.steps.head.traversedForward === false) + assert(paths.head.nodes.map(_.vertexGroupName) === Vector("Company", "Person")) + } + + test("enumeratePaths: undirected pattern over a DIRECTED self-loop -> both orientations") { + // FOLLOWS is directed: (a follows b) and (b follows a) are distinct matches, + // so an undirected match must surface BOTH as separate paths. + val (nodes, edges) = parsed("MATCH (a:Person)-[:FOLLOWS]-(b:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, mixedSchema, options) + assert(paths.length === 2) + assert(paths.map(_.steps.head.traversedForward).toSet === Set(true, false)) + paths.foreach { p => + assert(p.steps.head.edge.edgeGroupName === "FOLLOWS") + assert(p.nodes.map(_.vertexGroupName) === Vector("Person", "Person")) + } + } + + test( + "enumeratePaths: undirected pattern over an UNDIRECTED self-loop is de-duplicated to one path") { + // KNOWS is isDirected = false: getData already unions both orientations, so the + // resolver must emit a SINGLE forward path -- a second path would double-count. + val (nodes, edges) = parsed("MATCH (a:Person)-[:KNOWS]-(b:Person)") + val paths = Resolver.enumeratePaths(nodes, edges, mixedSchema, options) + assert(paths.length === 1) + assert(paths.head.steps.head.edge.edgeGroupName === "KNOWS") + assert(paths.head.steps.head.traversedForward === true) + } + + test("enumeratePaths: untyped undirected edge from Person fans out, with self-loop dedup") { + val (nodes, edges) = parsed("MATCH (a:Person)-[]-(b)") + val paths = Resolver.enumeratePaths(nodes, edges, mixedSchema, options) + // forward (outgoing): KNOWS->Person, FOLLOWS->Person, WORKS_AT->Company + // backward (incoming, dst==Person): KNOWS (dropped: undirected self-loop), FOLLOWS (kept) + val sig = + paths.map(p => (p.steps.head.edge.edgeGroupName, p.steps.head.traversedForward)).toSet + assert( + sig === Set(("KNOWS", true), ("FOLLOWS", true), ("WORKS_AT", true), ("FOLLOWS", false))) + assert(!sig.contains(("KNOWS", false))) // the undirected self-loop's backward copy is gone + assert(paths.length === 4) + } + + test("enumeratePaths: undirected disconnected pattern yields no paths") { + val (nodes, edges) = parsed("MATCH (a:Company)-[:WORKS_AT]-(b:Company)") + assert(Resolver.enumeratePaths(nodes, edges, mixedSchema, options).isEmpty) + } + + // --- Variable-length patterns ------------------------------------------- + // + // A bounded `*lo..hi` edge desugars into the union of fixed-length paths (one per length in + // [lo, hi]) with anonymous intermediate nodes. These tests use the `schema` fixture, whose + // KNOWS edge is a Person->Person self-loop, so a single var-length edge produces several + // distinct path lengths over the same group. + + test("var-length *1..3 over a self-loop expands to one path per length (1, 2, 3)") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS*1..3]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.paths.length === 3) + assert(rq.paths.map(_.length).sorted === Vector(1, 2, 3)) + rq.paths.foreach { p => + assert(p.nodes.forall(_.vertexGroupName == "Person")) + assert(p.steps.forall(s => s.edge.edgeGroupName == "KNOWS" && s.traversedForward)) + } + } + + test("var-length keeps endpoint variables and makes intermediate nodes anonymous") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS*1..2]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + + // The 2-hop path is (a) - KNOWS -> (anon) - KNOWS -> (b). + val twoHop = rq.paths.find(_.length == 2).getOrElse(fail("expected a 2-hop path")) + assert(twoHop.nodes.head.variable === Some("a")) + assert(twoHop.nodes.last.variable === Some("b")) + assert(twoHop.nodes(1).variable === None) + // The synthetic step carries no edge variable. + assert(twoHop.steps.forall(_.variable.isEmpty)) + } + + test("var-length *2..2 resolves to exactly the two-hop path") { + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS*2..2]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.paths.length === 1) + assert(rq.paths.head.length === 2) + assert(rq.paths.head.nodes(1).variable === None) + } + + test("var-length *N (exact) matches exactly N hops, not 1..N") { + // `*3` means EXACTLY three hops -> a single 3-hop path, NOT the union of 1,2,3. + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS*3]->(b:Person)") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.paths.length === 1) + assert(rq.paths.head.length === 3) + } + + test("var-length endpoint label filters the final node group") { + // KNOWS only ever lands on Person, so a Company endpoint yields no paths. + val ast = AstBuilder.parse("MATCH (a:Person)-[:KNOWS*1..2]->(b:Company)") + val rq = Resolver.resolve(ast, schema, options) + assert(rq.paths.isEmpty) + } + + test("untyped var-length fans out over candidate edge groups at every hop") { + // (a:Person)-[*1..2]->(x): + // length 1: Person-KNOWS->Person, Person-WORKS_AT->Company (2 paths) + // length 2: Person-KNOWS->Person-{KNOWS->Person, WORKS_AT->Company}, + // Person-WORKS_AT->Company-LOCATED_IN->City (3 paths) + val ast = AstBuilder.parse("MATCH (a:Person)-[*1..2]->(x)") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.paths.length === 5) + assert(rq.paths.count(_.length == 1) === 2) + assert(rq.paths.count(_.length == 2) === 3) + } + + test("var-length spliced before a following fixed hop") { + // (a:Person)-[:KNOWS*1..2]->(b:Person)-[:WORKS_AT]->(c:Company) + // length 2: KNOWS, WORKS_AT + // length 3: KNOWS, KNOWS, WORKS_AT + val ast = + AstBuilder.parse("MATCH (a:Person)-[:KNOWS*1..2]->(b:Person)-[:WORKS_AT]->(c:Company)") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.paths.length === 2) + assert(rq.paths.map(_.length).sorted === Vector(2, 3)) + rq.paths.foreach { p => + assert(p.steps.last.edge.edgeGroupName === "WORKS_AT") + assert(p.steps.init.forall(_.edge.edgeGroupName == "KNOWS")) + assert(p.nodes.last.vertexGroupName === "Company") + } + } + + test("scan-local WHERE on a var-length endpoint is attached to that endpoint only") { + val ast = + AstBuilder.parse("MATCH (a:Person)-[:KNOWS*1..2]->(b:Person) WHERE a.age > 30") + val rq = Resolver.resolve(ast, schema, options) + + assert(rq.joinPredicates === Nil) + assert(rq.postFilters === Nil) + rq.paths.foreach { p => + // `a` is always node 0; the predicate rides there. Intermediates/`b` carry nothing. + assert(p.nodes.head.scanFilter.length === 1) + assert(p.nodes.tail.forall(_.scanFilter.isEmpty)) + } + } + + test("var-length hi above maxVarLength is rejected") { + // Intended type is a parse/validation error; assert rejection regardless of the exact type. + intercept[Exception] { + Resolver.resolve( + AstBuilder.parse("MATCH (a:Person)-[:KNOWS*1..6]->(b:Person)"), + schema, + options + ) // default maxVarLength = 5 + } + } + + test("var-length hi above a custom maxVarLength is rejected (bound is on hi, not the span)") { + // *1..3 has span 2 but max-hops 3; with maxVarLength = 2 it must be rejected. + intercept[Exception] { + Resolver.resolve( + AstBuilder.parse("MATCH (a:Person)-[:KNOWS*1..3]->(b:Person)"), + schema, + QueryOptions(maxVarLength = 2)) + } + } + + test("var-length lo below 1 is rejected") { + // `*0..2` must be rejected (no zero-length hop in v1), not produce a malformed path. + intercept[Exception] { + Resolver.resolve( + AstBuilder.parse("MATCH (a:Person)-[:KNOWS*0..2]->(b:Person)"), + schema, + options) + } + } + + test("var-length lo greater than hi is rejected") { + // `*3..1` is an empty range; it must fail rather than silently yield no paths. + intercept[Exception] { + Resolver.resolve( + AstBuilder.parse("MATCH (a:Person)-[:KNOWS*3..1]->(b:Person)"), + schema, + options) + } + } +} diff --git a/core/src/test/scala/org/graphframes/propertygraph/internal/SchemaGraphSnapshotSuite.scala b/core/src/test/scala/org/graphframes/propertygraph/internal/SchemaGraphSnapshotSuite.scala new file mode 100644 index 000000000..9c0d03ad4 --- /dev/null +++ b/core/src/test/scala/org/graphframes/propertygraph/internal/SchemaGraphSnapshotSuite.scala @@ -0,0 +1,149 @@ +package org.graphframes.propertygraph.internal + +import org.apache.spark.sql.functions.lit +import org.graphframes.GraphFrame +import org.graphframes.GraphFrameTestSparkContext +import org.graphframes.SparkFunSuite +import org.graphframes.propertygraph.PropertyGraphFrame +import org.graphframes.propertygraph.property.EdgePropertyGroup +import org.graphframes.propertygraph.property.VertexPropertyGroup + +class SchemaGraphSnapshotSuite extends SparkFunSuite with GraphFrameTestSparkContext { + import sqlImplicits._ + + test("toDOT returns valid and deterministic DOT output") { + val snapshot = SchemaGraphSnapshot( + vertexGroupNames = Set("movies", "people", "genres"), + edges = Vector( + SchemaEdge("likes", "people", "movies", true), + SchemaEdge("belongs_to", "movies", "genres", true), + SchemaEdge("follows", "people", "people", true))) + + val dot = SchemaGraphSnapshot.toDOT(snapshot) + + val expected = + """digraph SchemaGraph { + | "genres"; + | "movies"; + | "people"; + | "movies" -> "genres" [label="belongs_to"]; + | "people" -> "movies" [label="likes"]; + | "people" -> "people" [label="follows"]; + |}""".stripMargin + + assert(dot === expected) + } + + test("toDOT escapes quotes and backslashes in names") { + val snapshot = SchemaGraphSnapshot( + vertexGroupNames = Set("v\"1", "path\\node"), + edges = Vector(SchemaEdge("edge\"name", "v\"1", "path\\node", true))) + + val dot = SchemaGraphSnapshot.toDOT(snapshot) + + val expected = + """digraph SchemaGraph { + | "path\\node"; + | "v\"1"; + | "v\"1" -> "path\\node" [label="edge\"name"]; + |}""".stripMargin + + assert(dot === expected) + } + + test("fromPropertyGraphFrame builds empty schema snapshot") { + val users = VertexPropertyGroup("users", Seq.empty[(Long)].toDF("id"), "id") + val pgf = PropertyGraphFrame(Seq(users), Seq.empty) + + val snapshot = SchemaGraphSnapshot.fromPropertyGraphFrame(pgf) + + assert(snapshot.vertexGroupNames === Set("users")) + assert(snapshot.edges === Vector.empty) + assert(snapshot.outgoing === Map.empty) + assert(snapshot.incoming === Map.empty) + } + + test("toString returns human-readable and deterministic schema description") { + val snapshot = SchemaGraphSnapshot( + vertexGroupNames = Set("movies", "people", "genres"), + edges = Vector( + SchemaEdge("likes", "people", "movies", true), + SchemaEdge("belongs_to", "movies", "genres", true), + SchemaEdge("follows", "people", "people", true))) + + val description = SchemaGraphSnapshot.toString(snapshot) + + val expected = + """Property graph schema: + |Vertex property groups (3): + | - genres + | - movies + | - people + |Edge property groups (3): + | - belongs_to: movies -> genres + | - likes: people -> movies + | - follows: people -> people""".stripMargin + + assert(description === expected) + } + + test("toString renders empty graph schema sections") { + val snapshot = SchemaGraphSnapshot(vertexGroupNames = Set.empty, edges = Vector.empty) + + val description = SchemaGraphSnapshot.toString(snapshot) + + val expected = + """Property graph schema: + |Vertex property groups (0): + | (none) + |Edge property groups (0): + | (none)""".stripMargin + + assert(description === expected) + } + + test("fromPropertyGraphFrame extracts vertex and edge group schema") { + val users = VertexPropertyGroup("users", Seq.empty[(Long)].toDF("id"), "id") + val posts = VertexPropertyGroup("posts", Seq.empty[(Long)].toDF("id"), "id") + + val writes = EdgePropertyGroup( + name = "writes", + data = Seq.empty[(Long, Long)].toDF("src", "dst"), + srcPropertyGroup = users, + dstPropertyGroup = posts, + isDirected = true, + srcColumnName = "src", + dstColumnName = "dst", + weightColumn = lit(1.0)) + + val follows = EdgePropertyGroup( + name = "follows", + data = Seq.empty[(Long, Long, Double)].toDF("src", "dst", "weight"), + srcPropertyGroup = users, + dstPropertyGroup = users, + isDirected = false, + srcColumnName = GraphFrame.SRC, + dstColumnName = GraphFrame.DST, + weightColumnName = GraphFrame.WEIGHT) + + val pgf = PropertyGraphFrame(Seq(users, posts), Seq(writes, follows)) + + val snapshot = SchemaGraphSnapshot.fromPropertyGraphFrame(pgf) + + assert(snapshot.vertexGroupNames === Set("users", "posts")) + assert( + snapshot.edges === Vector( + SchemaEdge("writes", "users", "posts", true), + SchemaEdge("follows", "users", "users", false))) + + assert( + snapshot.outgoing === Map( + "users" -> Vector( + SchemaEdge("writes", "users", "posts", true), + SchemaEdge("follows", "users", "users", false)))) + assert( + snapshot.incoming === Map( + "posts" -> Vector(SchemaEdge("writes", "users", "posts", true)), + "users" -> Vector(SchemaEdge("follows", "users", "users", false)))) + } +} diff --git a/project/GraphFramesAntlr4Plugin.scala b/project/GraphFramesAntlr4Plugin.scala new file mode 100644 index 000000000..c2c4fdd21 --- /dev/null +++ b/project/GraphFramesAntlr4Plugin.scala @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.antlr.v4.Tool +import sbt._ +import sbt.Keys._ + +/** + * Minimal internal sbt plugin that generates ANTLR4 Java sources from `.g4` + * grammar files. + * + * It exposes a single task, [[GraphFramesAntlr4Plugin.autoImport.antlr4Generate]], + * which invokes the ANTLR4 tool (resolved as a compile-time dependency of the + * meta-build in `project/plugins.sbt`) to emit Java sources for a lexer and a + * parser grammar. + * + * Design notes: + * - The ANTLR4 tool is a build-time-only dependency; the antlr4-runtime is + * provided transitively by Spark SQL at runtime, so no new dependency is + * added to the application build. + * - The parser is generated with `-visitor -no-listener`: the AST is built + * with a visitor (the AstBuilder extends the generated `*BaseVisitor`), so + * the default listener would be dead weight. + * - Generated sources should be emitted into a named package (set + * `antlr4GenPackage`); otherwise the unnamed-package classes cannot be + * imported from packaged Scala/Java sources. + * - The lexer must be generated before the parser, because the parser grammar + * references the lexer via `options { tokenVocab = GqlLexer; }`. We point + * ANTLR's `-lib` at the output directory so the freshly emitted + * `GqlLexer.tokens` is found. + * - The tool version is selected in `project/plugins.sbt` and must match the + * antlr4-runtime bundled with the targeted Spark major (3.5.x -> 4.9.3, + * 4.x -> 4.13.1); the generated ATN format is version-locked to it. + * + * The plugin is opt-in: enable it with `.enablePlugins(GraphFramesAntlr4Plugin)`. + */ +object GraphFramesAntlr4Plugin extends AutoPlugin { + + object autoImport { + val antlr4Generate = + TaskKey[Unit]("antlr4Generate", "Generate ANTLR4 Java sources from the .g4 grammar files.") + val antlr4GrammarDir = + SettingKey[File]("antlr4GrammarDir", "Directory containing the .g4 grammar files.") + val antlr4LexerGrammar = + SettingKey[File]("antlr4LexerGrammar", "Path to the lexer .g4 file.") + val antlr4ParserGrammar = + SettingKey[File]("antlr4ParserGrammar", "Path to the parser .g4 file.") + val antlr4OutputDir = + SettingKey[File]("antlr4OutputDir", "Output directory for generated ANTLR4 sources.") + val antlr4GenPackage = + SettingKey[Option[String]]( + "antlr4GenPackage", + "Optional Java package to generate into (ANTLR -package option). None by default.") + } + + import autoImport._ + + override def requires: Plugins = sbt.plugins.JvmPlugin + override def trigger: PluginTrigger = noTrigger + + override def projectSettings: Seq[Setting[_]] = Seq( + // Defaults point at the conventional GraphFrames grammar location. + antlr4GrammarDir := (Compile / baseDirectory).value / "src" / "main" / "antlr4" / + "org" / "graphframes" / "propertygraph" / "internal", + antlr4LexerGrammar := antlr4GrammarDir.value / "GqlLexer.g4", + antlr4ParserGrammar := antlr4GrammarDir.value / "GqlParser.g4", + antlr4OutputDir := target.value / "generated-sources" / "antlr4", + antlr4GenPackage := None, + antlr4Generate := { + val log = streams.value.log + val outDir = antlr4OutputDir.value + val lexerG4 = antlr4LexerGrammar.value + val parserG4 = antlr4ParserGrammar.value + val pkgOpt = antlr4GenPackage.value + + if (!lexerG4.isFile) { + sys.error(s"ANTLR4 lexer grammar not found: $lexerG4") + } + if (!parserG4.isFile) { + sys.error(s"ANTLR4 parser grammar not found: $parserG4") + } + + IO.createDirectory(outDir) + // The parser imports the lexer's token vocab, so the lexer is generated + // first; the generated GqlLexer.tokens then lives in the output dir. + log.info(s"ANTLR4: generating lexer from ${lexerG4.getName} into $outDir") + runAntlr(lexerG4, outDir, pkgOpt, libDir = None, genVisitor = false) + log.info(s"ANTLR4: generating parser from ${parserG4.getName} into $outDir") + runAntlr(parserG4, outDir, pkgOpt, libDir = Some(outDir), genVisitor = true) + }, + // Register the generated Java as managed Compile sources, and regenerate + // them automatically before compile. Returning the files from + // sourceGenerators is what actually makes them part of the Compile sources + // (the output dir is intentionally NOT added to unmanagedSourceDirectories + // to avoid double-counting). + Compile / sourceGenerators += Def.task { + antlr4Generate.value + (antlr4OutputDir.value ** "*.java").get + }.taskValue, + ) + + /** + * Invoke the ANTLR4 tool on a single grammar file. Throws on any error. + * + * @param grammar the `.g4` file to process + * @param outDir the `-o` output directory + * @param pkgOpt optional `-package` argument + * @param libDir optional `-lib` directory (where to find imported `.tokens`) + * @param genVisitor when true, emit a visitor and suppress the listener + * (`-visitor -no-listener`). The AST is built with a visitor, + * so the listener is dead weight. These options are + * parser-grammar only; pass false for the lexer. + */ + private def runAntlr( + grammar: File, + outDir: File, + pkgOpt: Option[String], + libDir: Option[File], + genVisitor: Boolean): Unit = { + val args = scala.collection.mutable.ArrayBuffer.empty[String] + args += "-o" + args += outDir.getAbsolutePath + libDir.foreach { lib => + args += "-lib" + args += lib.getAbsolutePath + } + pkgOpt.foreach { pkg => + args += "-package" + args += pkg + } + if (genVisitor) { + args += "-visitor" + args += "-no-listener" + } + args += grammar.getAbsolutePath + + val tool = new Tool(args.toArray) + // processGrammarsOnCommandLine returns normally even on grammar errors; + // the error count tells us whether anything went wrong. + tool.processGrammarsOnCommandLine() + if (tool.getNumErrors > 0) { + sys.error(s"ANTLR4 reported ${tool.getNumErrors} error(s) while processing ${grammar.getName}") + } + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 245f22ce8..8bc15e3d1 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -13,6 +13,21 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.8") libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.10.10" +// ANTLR4 tool used by the internal GraphFramesAntlr4Plugin to generate the GQL +// parser/lexer Java sources. This is a build-time-only dependency; the +// antlr4-runtime stays "provided" transitively via Spark SQL. +// +// The tool version MUST match the antlr4-runtime bundled with the targeted +// Spark major (the generated ATN format is version-locked to it): +// Spark 3.5.x -> 4.9.3, Spark 4.x -> 4.13.1 +// The default "3.5.8" mirrors build.sbt's sparkVer default. +val antlr4ToolVersion = sys.props.getOrElse("spark.version", "3.5.8").substring(0, 1) match { + case "4" => "4.13.1" + case "3" => "4.9.3" + case v => throw new IllegalArgumentException(s"Unsupported Spark version major: $v") +} +libraryDependencies += "org.antlr" % "antlr4" % antlr4ToolVersion + // Scalafix addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.5") diff --git a/python/poetry.lock b/python/poetry.lock index aba2d4417..ffb121671 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -14,19 +14,116 @@ files = [ [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, ] [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] +[[package]] +name = "backports-zstd" +version = "1.6.0" +description = "Backport of compression.zstd" +optional = false +python-versions = "<3.14,>=3.10" +groups = ["tutorials"] +markers = "python_version < \"3.14\"" +files = [ + {file = "backports_zstd-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:73000459db113a658c4fb0510100ef0e79137b5828bf957b7709aacae4eb1b87"}, + {file = "backports_zstd-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6e78d5e28f812b39f92397806ecddd4a6f3bf35531a8c039a1f187abc931af8"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:32f04d54ec1fdf3aa648b24a10b1c9234ed2046cc4af7a8850cbc236c05d42f3"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83415af3c64550a56cc20b4cce59bbaa81f21d28466d7adf98feff011ecbc66d"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3c17e6a267d13de9cbf14bf2ebfa87e03d26692456fc67d2dbed9da4f479b18"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:75578c71644b031118ce938855a53530708db7f4af6e83e2f8840d5a1de990f8"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4ae7ed5a6d813450cc2d818284ea3db9721edcef50a56aae42ea06feec38c6e"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5e9a8370c8ed873083d5de956d6b2e60adbad31e52d7a11111c96ef01d1910ae"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c2d1ccfe088e8279d605011a3575619a74526c261be357695b3258c0f636115a"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e73a550dbeb84e8fa50f8385f7735e9a4735b465851ef617d02f80ab10e44e7e"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:84f92e5a60a78c72ccda79d0417d311a1f6da18f446423ed411726d545bf7b56"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0eb4281f402b94d397b7482f6d9efd04c28274e4ed6eb57eb1f87bdd091a6a87"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d6b9b06323e3ba947c0003b2d70e02f33c90c36bc6262a92eb8201afc4a1aa08"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8872a0e9f1af975966b5be6af7eebd3dc4046f15e470b719316516dc3d137cd6"}, + {file = "backports_zstd-1.6.0-cp310-cp310-win32.whl", hash = "sha256:c14fa5dc39a804f1b92d63506f450eca5c59647a18d197d1a564b89dac1be1ce"}, + {file = "backports_zstd-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8219d6fceae6b39535c4ac323dba0923d10f781d59962ff3504e693fdcafa92c"}, + {file = "backports_zstd-1.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:b7bc9a0b66097f03820a54316d2fdd0beb38859cf98f10d63e94c55450ed8920"}, + {file = "backports_zstd-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4fc41b2df5529cad5ceb230319e82728096d4b353ce8d4df68a2ec37e291bb8"}, + {file = "backports_zstd-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:83391ef5935cc0f329b1abca414ae20ffe40d335fc21a4b5e664f08a74317d5f"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:7d3f64c503af7b60115b97c16feaf75bd191ef2c978d5c0c7725a6682bef63c5"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0308990ffc998df3c7ed35276bde049728b5c3956203cae40d80893576a41459"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c298785e2fadeab82342040f2d9ce764ce500e6da6a6d99a2de514e63580b5a"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae106fe16e36efc60ab098d02478d30aa0e31e1420eb4ecf0116459253bc6361"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7293fefe15f0e5852bdb4ad1e0e26f3cbd4d3e61c19f751ecc4ff34bc1eb237d"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ece8e7288db5b827ef8c64b2f78519f1a173a8991a625978fce02eccd7654fe9"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28eef3881164f3c23ce58ed59e4684103bdd279583eb2d299858c9e9b72fde9a"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:481a1e9bd8f419fdc625307aa20234687f99368c75df511ef589693c5fea4c6f"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3b6713371f8987a1178df93cb36f29eef191f224021e2d656b2f11ce60d26816"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b0ddbcd2866b8ff1a2836e4b0e4d44788f5b992d83fac75a38cda8f9a2bee079"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2914abea516704bdafb2090acd3f15b5f9debecfabd15b8dd8285b2ad3b92209"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd085eafa2aac6f883afd28210a3231f717f25409a1e44a39bb7b04c8c5b5646"}, + {file = "backports_zstd-1.6.0-cp311-cp311-win32.whl", hash = "sha256:b81b4cf3d6e0ad7ac92bef248f49fafc954262c5fb0f7e19d6aac497e5a856b2"}, + {file = "backports_zstd-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:10b61850c4112952e05aa6e6cce8c9a5936fbeadb321e154216705cc76a14afa"}, + {file = "backports_zstd-1.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:068ef3d8c18815a2e3a752f766313e19910e7c50939b956923748d9c04ebcb1b"}, + {file = "backports_zstd-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0466b14723f3b7697669c00ee66fe16e30e25636b286b0a923fa86fa3d8a753c"}, + {file = "backports_zstd-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d146926e997d2d3de8212bdcbf4985344a2622ca3bec458d8908000a84fd883"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:460fd6b3f338c659507ae36cfd6b58ac9942a2ff233c5cf574416dfec0451a84"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c2b1f4a640c51130caa92cef5bf72bd3c3dbbcfbf814c37403aa0601b1811b0"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:beb43e9885202c8d4f3762319ed4d5e98e197622afbff8439fbbdd81d08938b9"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fbb746522ebfc11155f1cd688e2c48ef3d74125e38b63eabdaab068a055c3e88"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a99710fbb225d459d66def4dc2bb2cd4a9a0bdc8b799fc0621cfdd863be9c93"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f69365ee2b836939137de024a302395a1cb8654fb6dc5ffef6381105259c8f87"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66cf8038893c7708ec345ffb3ac63c775d10f430f323ac2f0334fdb6a397c57c"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e514c71ca72f3b56bd8fbda1a6a5b7d1100a2764b42a3c74a38841f25f9b00ab"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7741e44f7938ec94f9a52678c8d19b7bc548522ffdc39c9e4481af8db545fa9a"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97e8a9674652496c7612b528085dd5a296c052a2edc466ca1bfb7b0b27820413"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:23a793f2fed4dbf0517319759a2cded0b0dd8e8d3797fe30badd5693e320c175"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b951113113ed4b8d173418a4f155c14b739dace626b3fa3f82be1831958d39e4"}, + {file = "backports_zstd-1.6.0-cp312-cp312-win32.whl", hash = "sha256:6430b34a2ae6fcc604672f4f913102563473d9a015bdca1ce8c95041cc1f2677"}, + {file = "backports_zstd-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:08793876172551a930ce4d65c712cd516184d1a97070d4a1193e05bf0cf7040d"}, + {file = "backports_zstd-1.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:03b7c59c71f7a597e2bcb3f8368371e9a660a1bdf1c37afc1f1ad1496a013c19"}, + {file = "backports_zstd-1.6.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:2ace939e4d620e119423606f2d3d7115f8707733bf57f279ad9a9383f875986f"}, + {file = "backports_zstd-1.6.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:4c68a9ed2df0cca51d774c521e68a34d2e3d9ebfc687ef8096adfd4f345b551d"}, + {file = "backports_zstd-1.6.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:30576f49b82328ec8af16c11100efe52ca88526f71bbe100ef6b4e707dc13bf2"}, + {file = "backports_zstd-1.6.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b4bddfcfb6679215d6f4dc5f79a1f9301af339480d70527a14b57a1f2e6b6cbf"}, + {file = "backports_zstd-1.6.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:65048ed08c5124f05ff9f355ab9703014bb2dbe7f8d9948ce193685b1775f442"}, + {file = "backports_zstd-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5918fc6b31437208721276964323933cd86077b8d5b469c59c1b3fd2c8220a05"}, + {file = "backports_zstd-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b6c8b02ab0ccb2431bb7bc238be91d158b308915e7b07937388e540466fe7e7"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:711e6b98f8924e8b4a61ff97ab6321f33de024e1ed6a32f5123763aeda8459be"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ba9ac10fc393e5123a08802e0e895a107cb4a66b9973d2844dbd8a343111e59"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f723219335387d7546412d8141e0303590600949b4184a1391a0c6a3c756058"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64b94d7a836568926a3309ff510c7f8261b881b341fd4992cabf4f0998878f8a"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e39258a09b1c7ca70b5e94a5c5ccfe4700b4250b8077cfeab31d0f79565d4c9b"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:15b1aae0f64cd742df4bba1d989d0a09a6ec619202543fdba684640454541fd3"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25b5ddc789480072551af571a746e9500356b2aff0499861cf2ca07ea7431e68"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a13cfa3410a75e4cb87abdb669aaf79da861cb79299159054ff8f77b9671bc40"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2ddab55a5f54dec8acfad68ef70f1c704fd21919990ddc238afbd6f496e61c6a"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fa305a84087e10d7a85e8a8a3dcba8cdbda4868f2180173b264b7b488fd37c55"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:df27b57d214a3124fbe4e933ef5a903d4567f154260d9aece8c797a987f2a205"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:28fecd73459d74910ae1987ab84b7bef690d3dd860948430dd5555108b006daf"}, + {file = "backports_zstd-1.6.0-cp313-cp313-win32.whl", hash = "sha256:3e689af303df287142770abe3a48bbefd24dab4a09da5807d0e1fa8c75bab026"}, + {file = "backports_zstd-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:b067b1ef9c8e41fb0882c828aa37829938b5c0dab067eca72b23fc24c563b9da"}, + {file = "backports_zstd-1.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:a838296f5b84c920172fb579cac894d255c1fc25457c7234613ddcfa385e49b7"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6c73ae37dbf9207727ac095dedef864c05d836eaec962a47b3b64eaadaf1c6b6"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:839faf90a7eb525a401978dc925df8c44bd12526e8ba1529b9f8a7106e729637"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f8f5c1c7c69a4b00889e52d9304a918a5b49010f9645768eb5fd0ad404f790ba"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e80bceebc9b58e959bede9b26cafe15b5b9526f3533a6dd06330c5da73cb9329"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79284c1dd702f4f24ed1a36e51555c907dd237b6c0d829595978f4089a2aeea9"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1e20b3ecd0a711be82e964aca28554eabbc31ee69a20e5e7b8fd42268af46212"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:aeef8563b82ed4af328f98e5041c1b4800d86f68f857ffd1577d4d47dc9aa6cd"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb75e33131946fabd6319061df3b8b1d588fe0963183280e9b5f49f7772fc09"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:ef132cfb638e9a86bd5dc07fb4e1cb895bc55bce6bb5e759366e8b160d0747e2"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab70eace272d6f122b121c057e436709b50a28abf30d97aab28433c08f4a4095"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17efb3d11137de5166dd51eedab9c36ad633402acba386eee8d715213ea47e49"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:994167ff6551b9c1ce226e0aab16295b98c94507b5701aa60d2c32b7d50796b1"}, + {file = "backports_zstd-1.6.0.tar.gz", hash = "sha256:80a7859ffe70bf239d7a2ce15293bdeb5b4280ff7dc326ffab312b0e254dbb24"}, +] + [[package]] name = "black" version = "23.12.1" @@ -187,250 +284,295 @@ files = [ [[package]] name = "brotlicffi" -version = "1.1.0.0" +version = "1.2.0.1" description = "Python CFFI bindings to the Brotli library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["tutorials"] markers = "platform_python_implementation == \"PyPy\"" files = [ - {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, - {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, - {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, - {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, - {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, - {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, - {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, - {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, - {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, - {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, - {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, - {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, - {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, - {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, - {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, - {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, - {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, - {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, - {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, - {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, - {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, - {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, - {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, - {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, - {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, - {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, - {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, + {file = "brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd"}, + {file = "brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5"}, + {file = "brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac"}, + {file = "brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec"}, + {file = "brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000"}, + {file = "brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4"}, + {file = "brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce"}, + {file = "brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a"}, + {file = "brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187"}, + {file = "brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede"}, + {file = "brotlicffi-1.2.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851"}, + {file = "brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37cb587d32bf7168e2218c455e22e409ad1f3157c6c71945879a311f3e6b6abf"}, + {file = "brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d6ba65dd528892b4d9960beba2ae011a753620bcfc66cf6fa3cee18d7b0baa4"}, + {file = "brotlicffi-1.2.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1"}, + {file = "brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c"}, ] [package.dependencies] -cffi = ">=1.0.0" +cffi = [ + {version = ">=1.0.0", markers = "python_version < \"3.13\""}, + {version = ">=1.17.0", markers = "python_version >= \"3.13\""}, +] [[package]] name = "certifi" -version = "2025.7.14" +version = "2026.6.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["docs", "tutorials"] files = [ - {file = "certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2"}, - {file = "certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995"}, + {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, + {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, ] [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["tutorials"] markers = "platform_python_implementation == \"PyPy\"" files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] [package.dependencies] -pycparser = "*" +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charset-normalizer" -version = "3.4.2" +version = "3.4.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["docs", "tutorials"] files = [ - {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, + {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, + {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, ] [[package]] name = "click" -version = "8.2.1" +version = "8.4.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["dev", "tutorials"] files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, + {file = "click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2"}, + {file = "click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"}, ] [package.dependencies] @@ -463,15 +605,15 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.3.0" +version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["connect", "dev"] markers = "python_version == \"3.10\"" files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, ] [package.dependencies] @@ -601,14 +743,14 @@ protobuf = ">=6.33.5,<8.0.0" [[package]] name = "idna" -version = "3.15" +version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["docs", "tutorials"] files = [ - {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, - {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, ] [package.extras] @@ -616,133 +758,109 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "imagesize" -version = "1.4.1" +version = "1.5.0" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" groups = ["docs"] +markers = "python_version >= \"3.14\"" files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, + {file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"}, + {file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"}, ] [[package]] -name = "inflate64" -version = "1.0.1" -description = "deflate64 compression/decompression library" +name = "imagesize" +version = "2.0.0" +description = "Get image size from headers (BMP/PNG/JPEG/JPEG2000/GIF/TIFF/SVG/Netpbm/WebP/AVIF/HEIC/HEIF)" optional = false -python-versions = ">=3.9" -groups = ["tutorials"] -markers = "python_version >= \"3.12\"" +python-versions = "<3.15,>=3.10" +groups = ["docs"] +markers = "python_version < \"3.14\"" files = [ - {file = "inflate64-1.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5122a188995e47a735ab969edc9129d42bbd97b993df5a3f0819b87205ce81b4"}, - {file = "inflate64-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:975ed694c680e46a5c0bb872380a9c9da271a91f9c0646561c58e8f3714347d4"}, - {file = "inflate64-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bcaf445d9cda5f7358e0c2b78144641560f8ce9e3e4351099754c49d26a34e8"}, - {file = "inflate64-1.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daede09baba24117279109b30fdf935195e91957e31b995b86f8dd01711376ee"}, - {file = "inflate64-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df40eaaba4fb8379d5c4fa5f56cc24741c4f1a91d4aef66438207473351ceaa"}, - {file = "inflate64-1.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ef90855ff63d53c8fd3bfbf85b5280b22f82b9ab2e21a7eee45b8a19d9866c42"}, - {file = "inflate64-1.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5daa4566c0b009c9ab8a6bf18ce407d14f5dbbb0d3068f3a43af939a17e117a7"}, - {file = "inflate64-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:d58a360b59685561a8feacee743479a9d7cc17c8d210aa1f2ae221f2513973cb"}, - {file = "inflate64-1.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31198c5f156806cee05b69b149074042b7b7d39274ff4c259b898e617294ac17"}, - {file = "inflate64-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ab693bb1cd92573a997f8fe7b90a2ec1e17a507884598f5640656257b95ef49"}, - {file = "inflate64-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:95b6a60e305e6e759e37d6c36691fcb87678922c56b3ddc2df06cd56e04f41f6"}, - {file = "inflate64-1.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:711ef889bdb3b3b296881d1e49830a3a896938fba7033c4287f1aed9b9a20111"}, - {file = "inflate64-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3178495970ecb5c6a32167a8b57fdeef3bf4e2843eaf8f2d8f816f523741e36"}, - {file = "inflate64-1.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e8373b7feedf10236eb56d21598a19a3eb51077c3702d0ce3456b827374025e1"}, - {file = "inflate64-1.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf026d5c885f2d2bbf233e9a0c8c6d046ec727e2467024ffe0ac76b5be308258"}, - {file = "inflate64-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:3aa7489241e6c6f6d34b9561efdf06031c35305b864267a5b8f406abcd3e85c5"}, - {file = "inflate64-1.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b81b3d373190ecd82901f42afd90b7127e9bdef341032a94db381c750ed3ddb2"}, - {file = "inflate64-1.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbfddc5dac975227c20997f0ac515917a15421767c6bff0c209ac6ff9d7b17cc"}, - {file = "inflate64-1.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2adeabe79cc2f90bca832673520c8cbad7370f86353e151293add7ca529bed34"}, - {file = "inflate64-1.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b235c97a05dbe2f92f0f057426e4d05a449e1fccf8e9aa88075ea9c6a06a182"}, - {file = "inflate64-1.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19b74e30734dca5f1c83ca07074e1f25bf7b63f4a5ee7e074d9a4cb05af65cd5"}, - {file = "inflate64-1.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b298feb85204b5ef148ccf807744c836fffed7c1ed3ec8bc9b4e323a03163291"}, - {file = "inflate64-1.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8a4c75241bc442267f79b8242135f2ded29405662c44b9353d34fbd4fa6e56b3"}, - {file = "inflate64-1.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:7b210392f0830ab27371e36478592f47757f5ea6c09ddb96e2125847b309eb5e"}, - {file = "inflate64-1.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8dd58aa1adc4f98bf9b52baffa8f2ddf589e071a90db2f2bec9024328d4608cf"}, - {file = "inflate64-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c108be2b87e88c966570f84f839eb37f489b45dc3fa3046dc228327af6e921bb"}, - {file = "inflate64-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63971c6b096c0d533c0e38b4257f5a7748501a8bc04d00cf239bd06467888703"}, - {file = "inflate64-1.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d0077edb6b1cabfa2223b71a4a725e5755148f551a7a396c7d5698e45fb8828"}, - {file = "inflate64-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f05b5f2a6f1bf2f70e9c20d997261711cbc1ae477379662b05b36911da60a67"}, - {file = "inflate64-1.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c7402165f7e15789caa0787e5a349465d9a454105d0c3a0ccf2e9cdfb8117"}, - {file = "inflate64-1.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39bced168822e4bf2f545d1b6dbeded6db01c32629d9e4549ef2cd1604a12e1b"}, - {file = "inflate64-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:70bb6a22d300d8ca25c26bc60afb5662c5a96d97a801962874d0461568512789"}, - {file = "inflate64-1.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f3d5ea758358a1cc50f9e8e41de2134e9b5c5ca8bbcd88d1cd135d0e953d0fa8"}, - {file = "inflate64-1.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fa102c834314c3d7edbf249d1be0bce5d12a9e122228a7ac3f861ee82c3dc5c"}, - {file = "inflate64-1.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c2ae56a34e6cc2a712418ac82332e5d550ef8599e0ffb64c19b86d63a7df0c5"}, - {file = "inflate64-1.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9808ae50b5db661770992566e51e648cac286c32bd80892b151e7b1eca81afe8"}, - {file = "inflate64-1.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:04b2788c6a26e1e525f53cc3d8c58782d41f18bef8d2a34a3d58beaaf0bfdd3b"}, - {file = "inflate64-1.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67fd5b1f9e433b0abab8cb91f4da94d16223a5241008268a57f4729fdbfc4dbc"}, - {file = "inflate64-1.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6f3b00c17ae365e82fc3d48ff9a7a566820a6c8c55b4e16c6cfbcbd46505a72"}, - {file = "inflate64-1.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:91c0c1d41c1655fb0189630baaa894a3b778d77062bb90ca11db878422948395"}, - {file = "inflate64-1.0.1.tar.gz", hash = "sha256:3b1c83c22651b5942b35829df526e89602e494192bf021e0d7d0b600e76c429d"}, + {file = "imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96"}, + {file = "imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3"}, ] -[package.extras] -check = ["check-manifest", "flake8", "flake8-black", "flake8-deprecated", "flake8-isort", "mypy (>=1.10.0)", "mypy_extensions (>=0.4.1)", "pygments", "readme-renderer", "twine"] -docs = ["docutils", "sphinx (>=5.0)"] -test = ["pytest"] - [[package]] name = "inflate64" -version = "1.0.3" +version = "1.0.4" description = "deflate64 compression/decompression library" optional = false -python-versions = "<3.14,>=3.9" +python-versions = ">=3.9" groups = ["tutorials"] -markers = "python_version < \"3.12\"" -files = [ - {file = "inflate64-1.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35abad47221ac8cb4cf6a9ef784916ada3f95115bd4f09e0f5f146b4463dcc93"}, - {file = "inflate64-1.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:518af99243b6cb4834c52c35b2965f38cc97aacbeb63d3e9cf820a9533957d37"}, - {file = "inflate64-1.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dad8527cb556fa3fb96dcf1631ea7c295bdc31ff05f2fb54363f6878c4eca9fa"}, - {file = "inflate64-1.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0efe4377c41a5b73f505d7ac613b85cd855d10b27e0e7e2ad3d7fceecfbb69a4"}, - {file = "inflate64-1.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8221b09edb0e94107b8c33da31f5e49ff9c49c96ab91956cc428891e39d2fd4e"}, - {file = "inflate64-1.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a0302b10bfed4b741be6221f52e83eabc337baf784dd0ca8ab8ca56458291952"}, - {file = "inflate64-1.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6df9b82179fc4616f315cd326a4d401649683b51314dcf59edac4cb5dcfc34d"}, - {file = "inflate64-1.0.3-cp310-cp310-win32.whl", hash = "sha256:8316c03d0d85de87bbff1641fb43a3367653beddaace3b50c35f49af5af8045c"}, - {file = "inflate64-1.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:8c88db9b53f31c06e99246af902643d6037912fd2ffb2ee58d12b3f705cad7d6"}, - {file = "inflate64-1.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:849cfc0f1395d9497a19356d90a41163471e2612a995c1cc0d39a1fc4fd4e442"}, - {file = "inflate64-1.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:faa2cb8301efc89ccb187ed4f7840ce0335da42ba53723077d0125b4b22789ce"}, - {file = "inflate64-1.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e47b63739611998142533e6e22cc3f947f4043e7e3bc7d70f94ffc4e4b1aa2b2"}, - {file = "inflate64-1.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0128fccc72259d99580ff60682575da66322eba8faa3dab4a75232b519defed"}, - {file = "inflate64-1.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dca452520ce5f286684577f02b06f669b9903380327d11231b5bdd3c6f27f2f"}, - {file = "inflate64-1.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9724aaa82a5181dd97f9e31134ede30f41a7add8b35073656e02fd16418c93c"}, - {file = "inflate64-1.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4b95b4866a5b5d1ca79f9c431f9f5d39586249c1b94985bdd5cc0704b70eebd2"}, - {file = "inflate64-1.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3009f85dec641b9af0b41ac0e5c849710e69c90955797ee760c2abfb302fa768"}, - {file = "inflate64-1.0.3-cp311-cp311-win32.whl", hash = "sha256:a36c0fa721acb800bbb9cbf5054ea0d9de4469e43b8b1fcd9d2bbb400ed4ccc0"}, - {file = "inflate64-1.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:8b14af86981e3f691bde23bd291e6bfe4114ddee50046a6f690d7a5b625c41b0"}, - {file = "inflate64-1.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:0856bdd440f7035803f25846015166bfd3daf59e659e43b6596cea37b389c839"}, - {file = "inflate64-1.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a25967a307aacf20ae979264fb7a1ad27b25a56fbc7e96dd28fcc12d54727479"}, - {file = "inflate64-1.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:54dc4d1a17084ff15127c5e88c8dd1aa57e48f292c1ac1f4c65f226b6fd93d9c"}, - {file = "inflate64-1.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d8fb0bc154147437df1d45c9466b2c06c745e49d238de356b709cd6b5c45769"}, - {file = "inflate64-1.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:478b51748e3754200a11520fccec836fa5719b7f7fb5f90d67594e9940978834"}, - {file = "inflate64-1.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cf0906c46a3224ffc96bd33c5fc108f4239c2552fbd1d10488a47ce7882465"}, - {file = "inflate64-1.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3dc36f76668d9174f17c27d19d0e95cb54cac0194ecb75cabbeed6244e75ab34"}, - {file = "inflate64-1.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f7f25b374af2c3d5d9fc016ad3907545d0a4a31c7765830f126da9fcbd5f04c9"}, - {file = "inflate64-1.0.3-cp312-cp312-win32.whl", hash = "sha256:9f5607153f294cb7ba37fdb6e744fe5c188d4b431fd6ff7b77530f39422eb026"}, - {file = "inflate64-1.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:34b793dd49fcc5c46e96e7156584de47fe9745ef2b45c4976f9c7764ea0137de"}, - {file = "inflate64-1.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:cd180b2a8709131a0768056f57cb0327813d55a214e76d7aed41b4413345a76b"}, - {file = "inflate64-1.0.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d5340fe27f88a2946968f7de1ebe996be6c8d59fd4a1ac00aacc5bcafcc6583"}, - {file = "inflate64-1.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:75b1625b027111270a5bb89fb6cb83930eacf4538881fb8ef901e00839272dc7"}, - {file = "inflate64-1.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1ced5841cbe81cb158c1fc0df7837e0f3c38b2f3b5b0c8f2a6490eb78b3a4f7a"}, - {file = "inflate64-1.0.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b89fddc67a3a2edc764cac2ef7cf0de76e2c98fce0800f55fa8974bcb01a10a9"}, - {file = "inflate64-1.0.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e32f7fb9c4120cdc27024249687fdaace2dc88857be6c031ae276d085a54166"}, - {file = "inflate64-1.0.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:14752a079cb4ab3d9653d39a818f2e0daf3c0b445efc332c343caeff908de2b7"}, - {file = "inflate64-1.0.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:14b811164f0c8048a90570c4213596eee277ab6454c86f1f80a5ace536e3b570"}, - {file = "inflate64-1.0.3-cp313-cp313-win32.whl", hash = "sha256:61a24f463e6dac38ddf2d4c011a54247f86cf676e869797de0e344ef7a4be456"}, - {file = "inflate64-1.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b077eaf7d6e99823751bd30e450102419cd71b6db4b3765e752e843fc040906"}, - {file = "inflate64-1.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:abc83da55d66d8e8cf1a782d5870f1aab4f2380d489af8c15825ee003645a974"}, - {file = "inflate64-1.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cca6c49371d159d4c6613860429777228d274b22a3d12d51fdf6c4d4397f0715"}, - {file = "inflate64-1.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:368c84e67b70b58e64952a47b89bd9938a1a3abf1bf7ad82fcfc57b53c1a21ba"}, - {file = "inflate64-1.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc779457ccb74b425251e3de1dba6839f78f19e0c25ae2e6178f270a334eae78"}, - {file = "inflate64-1.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dbc7265c5f6459e00b3c7cdf9de7c12f6a60f00ea8b878744d0ef46dd23d3f9"}, - {file = "inflate64-1.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81387fff4b3c72c46ed636aab092c7dbe7cdaf57b1f8f559885f0f933a5794b9"}, - {file = "inflate64-1.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:89460fbfbe383cb00291988343b5a4f0fb51aefaeacb1a5c01e9aa993a8ad17b"}, - {file = "inflate64-1.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:87101c97e7736eba8d3fccb364acc4efe77d977c6fbc048be11c5d99941f5651"}, - {file = "inflate64-1.0.3-cp39-cp39-win32.whl", hash = "sha256:3b35146f6d2680859cef3706cc205fe44c34e870613810baec56df0dc5786fde"}, - {file = "inflate64-1.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:8b41c8824a5c5446459b07607cf02e90115242cb0f16fa8ae6a7c882e4ece0a9"}, - {file = "inflate64-1.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:3352f3ca43c349df5b0217bc16c2d533b6344510e1e03acb9ed9a3c2a4db12b5"}, - {file = "inflate64-1.0.3.tar.gz", hash = "sha256:a89edd416c36eda0c3a5d32f31ff1555db2c5a3884aa8df95e8679f8203e12ee"}, +files = [ + {file = "inflate64-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1a47837d4322e0684824f91eb635aa6fd1967584140c478b0a1aca7b11740d6"}, + {file = "inflate64-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8600478542e2354d1ee7b5c57c957006cabacd8b787b4046951f487a2216e5c0"}, + {file = "inflate64-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb2b5a62579d074f38352a3494c3c6ac1a90516b75c5793c39303547f1fea925"}, + {file = "inflate64-1.0.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dcfafc572a642215894af1ec8d05949fa35eb7cb36d053aa97b11eccf1ae579e"}, + {file = "inflate64-1.0.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb93159cb60aee8cab62541aa70e4c460f13359660a27a1a486518bba0153535"}, + {file = "inflate64-1.0.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:89126ceb4d96e76842f4697017a9a3e750c34e029ddb360b3d8ca79a648d47f6"}, + {file = "inflate64-1.0.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f70e6692617ec82500b203eefac8765302298ce7e73584fcf995bb9e23184530"}, + {file = "inflate64-1.0.4-cp310-cp310-win32.whl", hash = "sha256:d08cdda33341b4f992af60c12dc60e370e9993b80a936c17244a602711eeb727"}, + {file = "inflate64-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:950dd7fe53474df5f4699b8f099980027e812d55fd82d8e167d599822c3d27d6"}, + {file = "inflate64-1.0.4-cp310-cp310-win_arm64.whl", hash = "sha256:bad20de249d6336793f6267880668dbb286ca5c6e6991795aa6344c817588068"}, + {file = "inflate64-1.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bccda9815b27623e805a34ee3ee4f46c93f0cc7ac621f9834d75f033fd79c27a"}, + {file = "inflate64-1.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c11e2a3cb9d9b49620c9b0c806dd0c55daec3b6bb665299b770a68f01bfc5432"}, + {file = "inflate64-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e42def03ace8c58fd50b0df4f40241c45a2314c3876d020cce24acf958323c98"}, + {file = "inflate64-1.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7912927a509ca58d1a445ce4ff6e6e9f276dc1d72687386cdf7103bf590e785c"}, + {file = "inflate64-1.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec40c0383cbd84d845dcb785a48ae76eef43246c923f84fda380fdd5ea653d3c"}, + {file = "inflate64-1.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b01539fea372c6078b9707d9121c12cb321e587e193f50e257ce06cf5b15e41"}, + {file = "inflate64-1.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bf4e34e32a37a42e9cf8bd9681f89e3e37b218f97d8b8cc95bd065419bc8db13"}, + {file = "inflate64-1.0.4-cp311-cp311-win32.whl", hash = "sha256:2725ccc14b138f0ad622d0322b769f177f9edfe016ee9ed3404102935d39e7de"}, + {file = "inflate64-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:7056148548c1f25dcb38251f88c19b4635a5f32af4c7bad00621c85509e3d8c0"}, + {file = "inflate64-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:2ea7bdcad65e255b4596f84880f6e0c1756d6336d620e302653257defa407742"}, + {file = "inflate64-1.0.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8009e4a4918ee6c8cbc49e58fe159464895064cfdf0565fed3f49ca81e45272"}, + {file = "inflate64-1.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d173a7a0e865bb7d19685c5b1ad2994712b8361b24136d7e94abeff58505647"}, + {file = "inflate64-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8bad992f2d034f5f7e36208e54502d1b0829ce772c898e5dc59109833420148a"}, + {file = "inflate64-1.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bfcf806912ced77a21394f7363805ecacd626b79f93cba87d505a48e88ede78"}, + {file = "inflate64-1.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62d1aac3aba094ae42e27ce7581b414c90f218248be0953b6aeb11a127225e5d"}, + {file = "inflate64-1.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8065166f355122484f004225b379d403346bdae69ec624786a9334f025580675"}, + {file = "inflate64-1.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94a95f32087d223d2e119ff5c7c264109e8d4cb7e421e7a688a899a6fe021b38"}, + {file = "inflate64-1.0.4-cp312-cp312-win32.whl", hash = "sha256:ad4fa490bb7dc2a4640a3adaa2d5950f4a465ba034bbcf184c2103646e58ad97"}, + {file = "inflate64-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:2c6befdf83d088a6e0d10d0873a9d4bfde2ce00ad7a52c8189cf303306f98030"}, + {file = "inflate64-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:2b263c619469f90a75f29c421c53d31b208ad494a078235a8f6db2bc96583fdc"}, + {file = "inflate64-1.0.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3f37540d0e64884a935fd62a7d17e40ab69f05ec63e815483b6513675d01bef"}, + {file = "inflate64-1.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4d24112180c95d12f279cade9a1e21f8be7f4790c4109c293292edf87d061992"}, + {file = "inflate64-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5c098dab17821f466fc6e6a3d78fc6e0295bb51458015f03416b1d58d6a8df4f"}, + {file = "inflate64-1.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a984b9287ff0fb596eb058d66a9e94530556afd2b7c054b44f2e0aeeff894e8f"}, + {file = "inflate64-1.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f62a13d0327631778fa2a47c308ae2b07b2659b7bb8564783259ac65949f8c0c"}, + {file = "inflate64-1.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:513201336fb3b0b7e2aee5dbbbe30a9f1b23291738b5ceb80076fc285f2ec2f1"}, + {file = "inflate64-1.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84ce3a97272ba745fce52b38363855c7201968f6402a794bbade774e64c657b9"}, + {file = "inflate64-1.0.4-cp313-cp313-win32.whl", hash = "sha256:332051a9d7e50579b90a3f555d68f53414b06f636c9ffe82e97c0baae3c8fbcc"}, + {file = "inflate64-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:3983f53b590ff7d0ba243f664ce852aca882482f30f7a8eab33e10d769336d0c"}, + {file = "inflate64-1.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:118d8286f085e99a14341c76ef9fbffd56619ccc80318a9a204aea3dbfa71470"}, + {file = "inflate64-1.0.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4f61925b2d4248eac2ebb15350a80aaa0d1f7f1dc770bd5ebbbb3b0db4a6a416"}, + {file = "inflate64-1.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c1acf18b08b32981a4a11ec5a112b8ad5d7c7a5b15cb5bdbdb5b19201e9aa180"}, + {file = "inflate64-1.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:abddae8920b2eaef824254e14b8d4ff54afbe6194a1bbe9816584859f0c1244d"}, + {file = "inflate64-1.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b303132cc562a906543a56f35c4e164e3880da6ff041cb4a7b1df9f9d2b4bb69"}, + {file = "inflate64-1.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f0993214dea0738c557fa56c13cd9083aef0097a201d726c21984ad7f577514"}, + {file = "inflate64-1.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a6baedc3288d7a4ff588951d3a9a97a5391dceed6255ff5b16e42cae7274bfa9"}, + {file = "inflate64-1.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a846ce1f38845b20bef2625af1b512be83416d97824539524c5a34e7a729aec7"}, + {file = "inflate64-1.0.4-cp314-cp314-win32.whl", hash = "sha256:eef87908c780439393d577a155868317f0a275b47b417db9f47d8633ec791745"}, + {file = "inflate64-1.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:fb2fdd63ef3933b67af98b3f2ee2f57e7787278041d7ba4821382fedd729b68a"}, + {file = "inflate64-1.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:2e129669a0243ac7816fd526946ee01c25688fe81623a6d6bc95b3156d80f4fb"}, + {file = "inflate64-1.0.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b17bf665d948dc4edeea0cd17752415d0cd7240c882b9c7e136ad4cc4321e9d4"}, + {file = "inflate64-1.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6751758301936fbb38fa38eb5312e14e27b6a1abf568f83c17557fab2694373d"}, + {file = "inflate64-1.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d6a4136752aa2a544301059d8f13780aeb88c34d60770258436a87dacd3fc304"}, + {file = "inflate64-1.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:938ebc6b28578bfd365d1a9fdb18b7faab08321babeb2198e8025d07d8dc7fb5"}, + {file = "inflate64-1.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61f51f80fa6f367288343c1a2cd20a42af454883087064e9274fd2a8c3a5a200"}, + {file = "inflate64-1.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:172b51da7bbfa66b33f0a5405e944807b9949e92cf4cd9f983c07af8152766df"}, + {file = "inflate64-1.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ca9a2985afd5a14fb48cd126a67e5944ccb7a0a6bdec58c4f796c8c88a84539"}, + {file = "inflate64-1.0.4-cp314-cp314t-win32.whl", hash = "sha256:f8964ceaabea294bc20abc9ef408c6aae978a75c25c83168a76cd87a37c38938"}, + {file = "inflate64-1.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:7d13b04cba65c12d21e65eaa77da9484e265e8e821b26e0761d1455ad3a878d9"}, + {file = "inflate64-1.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:9ae3ee727235a06dc3cd353ee5761fdd8e3b56ad119c711f61680528972a6ced"}, + {file = "inflate64-1.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:179e39069c56a69c37d3b939505254cf7e495a06fcbc0c4bd5a61fa8fc43c678"}, + {file = "inflate64-1.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:067af498c34a8b69b2957d20dbce4d72affda23aed37ea231b1ea5ad9aab5731"}, + {file = "inflate64-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e408d3b667fb693d45448327dc8cab8c684c6627d4fd8e71819f212ab1435e81"}, + {file = "inflate64-1.0.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b5aa704141c9bad08b4e57bd722f68dd3a8cff62490b16a1605d2698e268fbd"}, + {file = "inflate64-1.0.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a55295f493a40d7e68929f0ba54e489ab9b623b6aa968dc5d1389ba77a63eff"}, + {file = "inflate64-1.0.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e107d1d6e6d9b94409a68d1ac20522815996ad44269cc05a69c93d0f1c450f95"}, + {file = "inflate64-1.0.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:19ffca993315ce7a4439efc388ffebdd395a20dbea2942b2445d87ec15373737"}, + {file = "inflate64-1.0.4-cp39-cp39-win32.whl", hash = "sha256:dd8fdb7350728aa488edabeb9d2afbac5273522b50665e8dc99844a7eb99925b"}, + {file = "inflate64-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4662b2a4bba73bd64f803c97ec5134d6fd19227c3b3d3785e1babb359f1b4c33"}, + {file = "inflate64-1.0.4-cp39-cp39-win_arm64.whl", hash = "sha256:f5bb6c58c642859ecf5e23178b1e7e4724f3ce968fc49090919d59a5e186f3e6"}, + {file = "inflate64-1.0.4.tar.gz", hash = "sha256:b398c686960c029777afc0ed281a86f66adb956cfc3fbf6667cc6453f7b407ce"}, ] [package.extras] @@ -752,26 +870,26 @@ test = ["pytest"] [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["connect", "dev"] files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] [[package]] name = "isort" -version = "6.0.1" +version = "6.1.0" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.9.0" groups = ["dev"] files = [ - {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, - {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, + {file = "isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784"}, + {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"}, ] [package.extras] @@ -798,73 +916,101 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] [[package]] @@ -915,6 +1061,7 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" groups = ["connect", "dev"] +markers = "python_version == \"3.10\"" files = [ {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, @@ -973,76 +1120,224 @@ files = [ {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}, ] +[[package]] +name = "numpy" +version = "2.4.6" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["connect", "dev"] +markers = "python_version == \"3.11\"" +files = [ + {file = "numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4"}, + {file = "numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d"}, + {file = "numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8"}, + {file = "numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538"}, + {file = "numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47"}, + {file = "numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93"}, + {file = "numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8"}, + {file = "numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6"}, + {file = "numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8"}, + {file = "numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147"}, + {file = "numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698"}, + {file = "numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f"}, + {file = "numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853"}, + {file = "numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a"}, + {file = "numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2"}, + {file = "numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45"}, + {file = "numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751"}, + {file = "numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3"}, + {file = "numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b"}, + {file = "numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089"}, + {file = "numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a"}, + {file = "numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605"}, + {file = "numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91"}, + {file = "numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359"}, + {file = "numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778"}, + {file = "numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1"}, + {file = "numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe"}, + {file = "numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997"}, + {file = "numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20"}, + {file = "numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d"}, + {file = "numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67"}, + {file = "numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd"}, + {file = "numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab"}, + {file = "numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75"}, + {file = "numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096"}, + {file = "numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b"}, + {file = "numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8"}, + {file = "numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402"}, + {file = "numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb"}, + {file = "numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1"}, + {file = "numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261"}, + {file = "numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6"}, + {file = "numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a"}, + {file = "numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e"}, + {file = "numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e"}, + {file = "numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43"}, + {file = "numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e"}, + {file = "numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895"}, + {file = "numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4"}, + {file = "numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063"}, + {file = "numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627"}, + {file = "numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73"}, + {file = "numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda"}, +] + +[[package]] +name = "numpy" +version = "2.5.0" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.12" +groups = ["connect", "dev"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "numpy-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:489780423903667933b4ed6197b6ec3b75ea5dd17d1d8f0f38d798feb6921561"}, + {file = "numpy-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ece55976ced6bca95a03ae2839e2e5ccffe8eb6a3e7022415645eb154a81e4e6"}, + {file = "numpy-2.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c83b664b0e6eee9594fa920cf0639d8af796606d3fad6cc70180c87e4b97c7be"}, + {file = "numpy-2.5.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bf80333980bf37f523341ddd72c783f39d6829ec7736b9eb99086388a2d52cc2"}, + {file = "numpy-2.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a4874217b36d5ac8fc876f52e39df56f8182c88463e9e2dceabf7ca8b7efb8"}, + {file = "numpy-2.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaa760137137e8d3c920d27927748215b56014f92667dc9b6c27dfc61249255a"}, + {file = "numpy-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7174ce8265fc7f7417d171c9ea8fe905220748893ea67a2a7abe726ec331c4b0"}, + {file = "numpy-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b8c3daaf99de52415d20b42f8e8155c78642cb04207d02f9d317a0dcf1b3fb54"}, + {file = "numpy-2.5.0-cp312-cp312-win32.whl", hash = "sha256:6206db0af545d73d068add6d992279145f158428d1da6cc49adc4b630c5d6ee5"}, + {file = "numpy-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:6f2d6873e2940c860a309d21e25b1e69af6aaffdd80aa056b04c16380db1c4f2"}, + {file = "numpy-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:a55e1eb2bca2cfd17a16b213c99dfc8502d47b0d494224d2122277d0400935ca"}, + {file = "numpy-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:520e6b8be0a4b65840ac8090d4f51cef4bed66e2b0894d5a520f099adc24a9b2"}, + {file = "numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:146b81cdd3967fdb6beca8ba25f00c58741d8f3cbd797f55af0fbe0bfec3469c"}, + {file = "numpy-2.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:126b88d95e8ff9b00c9e717aa540469f21d6180162f84c0caec51b16215d49cd"}, + {file = "numpy-2.5.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d4313cef1594c5ce46c31b6e54e918338f63f16ee9322304e8c9114d6d81c8bd"}, + {file = "numpy-2.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:750fb097caf26fa878746d9d119f6f9da12dedcbff1eea966c3e3447647c4a9e"}, + {file = "numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3893adc2dc7c0412ba76777db55a049215d99c9aa3113003be8f49f4f1290ab9"}, + {file = "numpy-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:835e454dd99b238cdc5a3f63bce2371296f5ebc53ca1e0f8e6ddbb6d92a29aab"}, + {file = "numpy-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f9836778081a0a3c02a6a21493f3e9f5b311f8d2541934f31f05583dc999ea4"}, + {file = "numpy-2.5.0-cp313-cp313-win32.whl", hash = "sha256:0b525be4744b60bb0557ac872d53ef07d085b5f39622bc579c98d3809d05b988"}, + {file = "numpy-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:44353e2878930039db472b99dc353d749826e4010bd4d2a7f835e94a97a5c748"}, + {file = "numpy-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:48f54b00711f83a5f796b70c518e8c2b3c5848dda03a54911f23eb68519b9b60"}, + {file = "numpy-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f27582c55ba4c750b7c58c8faf021d2cd9324a662b466229db8a417b41368af9"}, + {file = "numpy-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:28e7137057d551e4a83c4ae414e3451f50568409db7569aacc7f9811ee06a446"}, + {file = "numpy-2.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e1da54b53e75cd9fcfc23efcc7edab2c6aecf97b6037566d8a0fe804af8ec57c"}, + {file = "numpy-2.5.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:694d8f74e156f7fd01179f1aa8faa2f648ab6ae0f70b6c3fe57a03249aea2303"}, + {file = "numpy-2.5.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a7569a7b53c77716f036bb28cb1c91f166a26ec7d9502cd1e4bdfe502fdec22"}, + {file = "numpy-2.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a0433bd4086ebd462960cf375e19195bb07b53dc1d87dd5fcf47ad78576f03"}, + {file = "numpy-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:929f0c79ac38bcbd7154fe631dc907abfeddbcc5027a896bd1f7767323271e7a"}, + {file = "numpy-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cc4f247a47bbf070bfd70be53ccdcf47b800af563535e7bbe172322197c30e21"}, + {file = "numpy-2.5.0-cp314-cp314-win32.whl", hash = "sha256:5dc71423499fab3f46f7a7201155ade1669ea101f2f429d332df9e72f8161731"}, + {file = "numpy-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ebb81d9d5443e0309d6c54894c3fbed74ad7da0714352a67b6d773cd189eae73"}, + {file = "numpy-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3b94d0d0deceebfad3e67ae5c0e5eb87371e8f7a0581cd04a779928c2450cf1e"}, + {file = "numpy-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:22f3d43e362d650bc39db1f17851302874a148ca95ba6981c1dfb5fa6862f35b"}, + {file = "numpy-2.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:243563efb4cd7528a264567e9fd206c87826457322521d06206a00bfa316c927"}, + {file = "numpy-2.5.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:84881d825ca75249b189bbee875fcfe3238aa5c479e6100893cda566e8e86826"}, + {file = "numpy-2.5.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cda12aa4779d42b8771180aba759c96f527d43446d8f380ab59e2b35e8489efd"}, + {file = "numpy-2.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c0121101093d2bd74981b10f8837d78e794a8ff57834eb27179f49e1ba11ac6"}, + {file = "numpy-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d371c92cfa09da00022f501ab67fafaea813d752eb30ac44336d45b1e5b0268a"}, + {file = "numpy-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9990713e9c38154c6861e7547f1e3fc7a87e75ff09bab24ef1cc81d81c2835e9"}, + {file = "numpy-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:edadfbd4794b1086c0d822f81863e8a68fc129d132fd0bb9e31e955d7fbbbdb7"}, + {file = "numpy-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f7e5fa4382967ae6548bd2f174219afb908e294b0d5f625af01166edd5f7d9aa"}, + {file = "numpy-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:016623417bb330d719d579daf2d6b9a01ddc52e41a9ed61a47f39fde46dcd865"}, + {file = "numpy-2.5.0.tar.gz", hash = "sha256:5a129578019311b6e56bdd714250f19b518f7dceeeb8d1af5490f4942d3f891c"}, +] + [[package]] name = "packaging" -version = "25.0" +version = "26.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["connect", "dev", "docs"] files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, ] [[package]] name = "pandas" -version = "2.3.1" +version = "2.3.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" groups = ["connect", "dev"] +markers = "python_version == \"3.10\"" files = [ - {file = "pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9"}, - {file = "pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1"}, - {file = "pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f951fbb702dacd390561e0ea45cdd8ecfa7fb56935eb3dd78e306c19104b9b0"}, - {file = "pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd05b72ec02ebfb993569b4931b2e16fbb4d6ad6ce80224a3ee838387d83a191"}, - {file = "pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1b916a627919a247d865aed068eb65eb91a344b13f5b57ab9f610b7716c92de1"}, - {file = "pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fe67dc676818c186d5a3d5425250e40f179c2a89145df477dd82945eaea89e97"}, - {file = "pandas-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eb789ae0274672acbd3c575b0598d213345660120a257b47b5dafdc618aec83"}, - {file = "pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b"}, - {file = "pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f"}, - {file = "pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85"}, - {file = "pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d"}, - {file = "pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678"}, - {file = "pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299"}, - {file = "pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab"}, - {file = "pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3"}, - {file = "pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232"}, - {file = "pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e"}, - {file = "pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4"}, - {file = "pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8"}, - {file = "pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679"}, - {file = "pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8"}, - {file = "pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22"}, - {file = "pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a"}, - {file = "pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928"}, - {file = "pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9"}, - {file = "pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12"}, - {file = "pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb"}, - {file = "pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956"}, - {file = "pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a"}, - {file = "pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9"}, - {file = "pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275"}, - {file = "pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab"}, - {file = "pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96"}, - {file = "pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444"}, - {file = "pandas-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4645f770f98d656f11c69e81aeb21c6fca076a44bed3dcbb9396a4311bc7f6d8"}, - {file = "pandas-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:342e59589cc454aaff7484d75b816a433350b3d7964d7847327edda4d532a2e3"}, - {file = "pandas-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d12f618d80379fde6af007f65f0c25bd3e40251dbd1636480dfffce2cf1e6da"}, - {file = "pandas-2.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd71c47a911da120d72ef173aeac0bf5241423f9bfea57320110a978457e069e"}, - {file = "pandas-2.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:09e3b1587f0f3b0913e21e8b32c3119174551deb4a4eba4a89bc7377947977e7"}, - {file = "pandas-2.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2323294c73ed50f612f67e2bf3ae45aea04dce5690778e08a09391897f35ff88"}, - {file = "pandas-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4b0de34dc8499c2db34000ef8baad684cfa4cbd836ecee05f323ebfba348c7d"}, - {file = "pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, + {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, + {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, + {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, + {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, + {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, + {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, + {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, ] [package.dependencies] -numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, -] +numpy = {version = ">=1.22.4", markers = "python_version < \"3.11\""} python-dateutil = ">=2.8.2" pytz = ">=2020.1" tzdata = ">=2022.7" @@ -1072,35 +1367,128 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "pandas" +version = "3.0.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.11" +groups = ["connect", "dev"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98"}, + {file = "pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639"}, + {file = "pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2"}, + {file = "pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27"}, + {file = "pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824"}, + {file = "pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938"}, + {file = "pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea"}, + {file = "pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a"}, + {file = "pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09"}, + {file = "pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4"}, + {file = "pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c"}, + {file = "pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9"}, + {file = "pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf"}, + {file = "pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c"}, + {file = "pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc"}, + {file = "pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49"}, + {file = "pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa"}, + {file = "pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7"}, + {file = "pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8"}, + {file = "pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a"}, + {file = "pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb"}, + {file = "pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2"}, + {file = "pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44"}, + {file = "pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e"}, + {file = "pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d"}, + {file = "pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066"}, + {file = "pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd"}, + {file = "pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085"}, + {file = "pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870"}, + {file = "pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f"}, + {file = "pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13"}, + {file = "pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac"}, + {file = "pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f"}, + {file = "pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb"}, + {file = "pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a"}, + {file = "pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360"}, + {file = "pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76"}, + {file = "pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5"}, + {file = "pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977"}, + {file = "pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04"}, + {file = "pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6"}, + {file = "pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c"}, + {file = "pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028"}, + {file = "pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d"}, + {file = "pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a"}, + {file = "pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1"}, + {file = "pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1"}, + {file = "pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.26.0", markers = "python_version < \"3.14\""}, + {version = ">=2.3.3", markers = "python_version >= \"3.14\""}, +] +python-dateutil = ">=2.8.2" +tzdata = {version = "*", markers = "sys_platform == \"win32\" or sys_platform == \"emscripten\""} + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "adbc-driver-sqlite (>=1.2.0)", "beautifulsoup4 (>=4.12.3)", "bottleneck (>=1.4.2)", "fastparquet (>=2024.11.0)", "fsspec (>=2024.10.0)", "gcsfs (>=2024.10.0)", "html5lib (>=1.1)", "hypothesis (>=6.116.0)", "jinja2 (>=3.1.5)", "lxml (>=5.3.0)", "matplotlib (>=3.9.3)", "numba (>=0.60.0)", "numexpr (>=2.10.2)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.5)", "psycopg2 (>=2.9.10)", "pyarrow (>=13.0.0)", "pyiceberg (>=0.8.1)", "pymysql (>=1.1.1)", "pyreadstat (>=1.2.8)", "pytest (>=8.3.4)", "pytest-xdist (>=3.6.1)", "python-calamine (>=0.3.0)", "pytz (>=2020.1)", "pyxlsb (>=1.0.10)", "qtpy (>=2.4.2)", "s3fs (>=2024.10.0)", "scipy (>=1.14.1)", "tables (>=3.10.1)", "tabulate (>=0.9.0)", "xarray (>=2024.10.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.2.0)", "zstandard (>=0.23.0)"] +aws = ["s3fs (>=2024.10.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.4.2)"] +compression = ["zstandard (>=0.23.0)"] +computation = ["scipy (>=1.14.1)", "xarray (>=2024.10.0)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.5)", "python-calamine (>=0.3.0)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.2.0)"] +feather = ["pyarrow (>=13.0.0)"] +fss = ["fsspec (>=2024.10.0)"] +gcp = ["gcsfs (>=2024.10.0)"] +hdf5 = ["tables (>=3.10.1)"] +html = ["beautifulsoup4 (>=4.12.3)", "html5lib (>=1.1)", "lxml (>=5.3.0)"] +iceberg = ["pyiceberg (>=0.8.1)"] +mysql = ["SQLAlchemy (>=2.0.36)", "pymysql (>=1.1.1)"] +output-formatting = ["jinja2 (>=3.1.5)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=13.0.0)"] +performance = ["bottleneck (>=1.4.2)", "numba (>=0.60.0)", "numexpr (>=2.10.2)"] +plot = ["matplotlib (>=3.9.3)"] +postgresql = ["SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "psycopg2 (>=2.9.10)"] +pyarrow = ["pyarrow (>=13.0.0)"] +spss = ["pyreadstat (>=1.2.8)"] +sql-other = ["SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "adbc-driver-sqlite (>=1.2.0)"] +test = ["hypothesis (>=6.116.0)", "pytest (>=8.3.4)", "pytest-xdist (>=3.6.1)"] +timezone = ["pytz (>=2020.1)"] +xml = ["lxml (>=5.3.0)"] + [[package]] name = "pathspec" -version = "0.12.1" +version = "1.1.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, ] +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] + [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, + {file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"}, + {file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"}, ] -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - [[package]] name = "pluggy" version = "1.6.0" @@ -1137,28 +1525,39 @@ files = [ [[package]] name = "psutil" -version = "7.0.0" -description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." +version = "7.2.2" +description = "Cross-platform lib for process and system monitoring." optional = false python-versions = ">=3.6" groups = ["tutorials"] markers = "sys_platform != \"cygwin\"" files = [ - {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, - {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, - {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, - {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, - {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, - {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, - {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, + {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, + {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, + {file = "psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63"}, + {file = "psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312"}, + {file = "psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b"}, + {file = "psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9"}, + {file = "psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00"}, + {file = "psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9"}, + {file = "psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a"}, + {file = "psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf"}, + {file = "psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1"}, + {file = "psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841"}, + {file = "psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486"}, + {file = "psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979"}, + {file = "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9"}, + {file = "psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e"}, + {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8"}, + {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc"}, + {file = "psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988"}, + {file = "psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee"}, + {file = "psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372"}, ] [package.extras] -dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] -test = ["pytest", "pytest-xdist", "setuptools"] +dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] +test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] [[package]] name = "py4j" @@ -1174,153 +1573,161 @@ files = [ [[package]] name = "py7zr" -version = "0.22.0" +version = "1.1.3" description = "Pure python 7-zip library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["tutorials"] files = [ - {file = "py7zr-0.22.0-py3-none-any.whl", hash = "sha256:993b951b313500697d71113da2681386589b7b74f12e48ba13cc12beca79d078"}, - {file = "py7zr-0.22.0.tar.gz", hash = "sha256:c6c7aea5913535184003b73938490f9a4d8418598e533f9ca991d3b8e45a139e"}, + {file = "py7zr-1.1.3-py3-none-any.whl", hash = "sha256:17934a35089e026dec6c72ee275d9b841e646881ef822d618e805c3006661d9a"}, + {file = "py7zr-1.1.3.tar.gz", hash = "sha256:8d51894abb38355bf14881088bb97f01fe4cb5b14ebe22f66d6297668c7e1a74"}, ] [package.dependencies] -brotli = {version = ">=1.1.0", markers = "platform_python_implementation == \"CPython\""} -brotlicffi = {version = ">=1.1.0.0", markers = "platform_python_implementation == \"PyPy\""} -inflate64 = ">=1.0.0,<1.1.0" +"backports.zstd" = {version = ">=1.0.0", markers = "python_version < \"3.14\""} +brotli = {version = ">=1.2.0", markers = "platform_python_implementation == \"CPython\""} +brotlicffi = {version = ">=1.2.0.0", markers = "platform_python_implementation == \"PyPy\""} +inflate64 = ">=1.0.4" multivolumefile = ">=0.2.3" psutil = {version = "*", markers = "sys_platform != \"cygwin\""} -pybcj = ">=1.0.0,<1.1.0" -pycryptodomex = ">=3.16.0" -pyppmd = ">=1.1.0,<1.2.0" -pyzstd = ">=0.15.9" +pybcj = ">=1.0.6" +pycryptodomex = ">=3.20.0" +pyppmd = ">=1.3.1" texttable = "*" [package.extras] -check = ["black (>=23.1.0)", "check-manifest", "flake8 (<8)", "flake8-black (>=0.3.6)", "flake8-deprecated", "flake8-isort", "isort (>=5.0.3)", "lxml", "mypy (>=0.940)", "mypy-extensions (>=0.4.1)", "pygments", "readme-renderer", "twine", "types-psutil"] +check = ["black (>=25.1.0)", "check-manifest", "flake8 (<8)", "flake8-black (>=0.3.6)", "flake8-deprecated", "flake8-isort", "isort (>=7.0.0)", "lxml", "mypy (>=1.17.0)", "mypy_extensions (>=1.1.0)", "pygments", "pylint", "readme-renderer", "twine", "types-psutil"] debug = ["pytest", "pytest-leaks", "pytest-profiling"] -docs = ["docutils", "sphinx (>=5.0)", "sphinx-a4doc", "sphinx-py3doc-enhanced-theme"] -test = ["coverage[toml] (>=5.2)", "coveralls (>=2.1.1)", "py-cpuinfo", "pytest", "pytest-benchmark", "pytest-cov", "pytest-remotedata", "pytest-timeout"] -test-compat = ["libarchive-c"] +docs = ["docutils", "sphinx (>=8.0.0)", "sphinx-a4doc", "sphinx-py3doc-enhanced-theme"] +test = ["coverage[toml] (>=7.10.7)", "coveralls (>=4.0.2)", "py-cpuinfo", "pytest", "pytest-benchmark", "pytest-cov", "pytest-httpserver", "pytest-remotedata", "pytest-timeout", "requests"] [[package]] name = "pyarrow" -version = "23.0.1" +version = "24.0.0" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.10" groups = ["connect", "dev"] files = [ - {file = "pyarrow-23.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:3fab8f82571844eb3c460f90a75583801d14ca0cc32b1acc8c361650e006fd56"}, - {file = "pyarrow-23.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:3f91c038b95f71ddfc865f11d5876c42f343b4495535bd262c7b321b0b94507c"}, - {file = "pyarrow-23.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d0744403adabef53c985a7f8a082b502a368510c40d184df349a0a8754533258"}, - {file = "pyarrow-23.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c33b5bf406284fd0bba436ed6f6c3ebe8e311722b441d89397c54f871c6863a2"}, - {file = "pyarrow-23.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ddf743e82f69dcd6dbbcb63628895d7161e04e56794ef80550ac6f3315eeb1d5"}, - {file = "pyarrow-23.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e052a211c5ac9848ae15d5ec875ed0943c0221e2fcfe69eee80b604b4e703222"}, - {file = "pyarrow-23.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5abde149bb3ce524782d838eb67ac095cd3fd6090eba051130589793f1a7f76d"}, - {file = "pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb"}, - {file = "pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350"}, - {file = "pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd"}, - {file = "pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9"}, - {file = "pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701"}, - {file = "pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78"}, - {file = "pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919"}, - {file = "pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f"}, - {file = "pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7"}, - {file = "pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9"}, - {file = "pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05"}, - {file = "pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67"}, - {file = "pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730"}, - {file = "pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0"}, - {file = "pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8"}, - {file = "pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f"}, - {file = "pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677"}, - {file = "pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2"}, - {file = "pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37"}, - {file = "pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2"}, - {file = "pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a"}, - {file = "pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1"}, - {file = "pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500"}, - {file = "pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41"}, - {file = "pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07"}, - {file = "pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83"}, - {file = "pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125"}, - {file = "pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8"}, - {file = "pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca"}, - {file = "pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1"}, - {file = "pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb"}, - {file = "pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1"}, - {file = "pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886"}, - {file = "pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f"}, - {file = "pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5"}, - {file = "pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d"}, - {file = "pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f"}, - {file = "pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814"}, - {file = "pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d"}, - {file = "pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7"}, - {file = "pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690"}, - {file = "pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce"}, - {file = "pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019"}, + {file = "pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb"}, + {file = "pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147"}, + {file = "pyarrow-24.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c"}, + {file = "pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041"}, + {file = "pyarrow-24.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491"}, + {file = "pyarrow-24.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1"}, + {file = "pyarrow-24.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591"}, + {file = "pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74"}, + {file = "pyarrow-24.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3"}, + {file = "pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868"}, + {file = "pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e"}, + {file = "pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57"}, + {file = "pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c"}, + {file = "pyarrow-24.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981"}, + {file = "pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810"}, + {file = "pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a"}, + {file = "pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66"}, + {file = "pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb"}, + {file = "pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e"}, + {file = "pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6"}, + {file = "pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826"}, + {file = "pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba"}, + {file = "pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68"}, + {file = "pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2"}, + {file = "pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0"}, + {file = "pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495"}, + {file = "pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f"}, + {file = "pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91"}, + {file = "pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275"}, + {file = "pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b"}, + {file = "pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42"}, + {file = "pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b"}, + {file = "pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37"}, + {file = "pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca"}, + {file = "pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d"}, + {file = "pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838"}, + {file = "pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b"}, + {file = "pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795"}, + {file = "pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26"}, + {file = "pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde"}, + {file = "pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76"}, + {file = "pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e"}, + {file = "pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05"}, + {file = "pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a"}, + {file = "pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072"}, + {file = "pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931"}, + {file = "pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699"}, + {file = "pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136"}, + {file = "pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19"}, + {file = "pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83"}, ] [[package]] name = "pybcj" -version = "1.0.6" +version = "1.0.7" description = "bcj filter library" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["tutorials"] files = [ - {file = "pybcj-1.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0fc8eda59e9e52d807f411de6db30aadd7603aa0cb0a830f6f45226b74be1926"}, - {file = "pybcj-1.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0495443e8691510129f0c589ed956af4962c22b7963c5730b0c80c9c5b818c06"}, - {file = "pybcj-1.0.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c7998b546c3856dbe9ae879cb90393df80507f65097e7019785852769f4a990"}, - {file = "pybcj-1.0.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c859f85e718924f48b3ac967cda5528ccbef1e448a4462652cca688eee477"}, - {file = "pybcj-1.0.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:186fbb849883ac80764d96dbd253503dd9cecbcf6133504a0c9d6a2df81d5746"}, - {file = "pybcj-1.0.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:437bd5f5e6579bde404404ad2de915d1306c389595c68d0eb8933fee1408e951"}, - {file = "pybcj-1.0.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:933d6be8f07c653ff3eba16900376b3212249be1c71caf9db17f4cd52da5076c"}, - {file = "pybcj-1.0.6-cp310-cp310-win_amd64.whl", hash = "sha256:90e169b669bbed30e22d36ba97d23dcfc71e044d3be41c8010fd6a53950725e5"}, - {file = "pybcj-1.0.6-cp310-cp310-win_arm64.whl", hash = "sha256:06441026c773f8abeb7816566acfffe7cd65a9b69094197a9de64d0496cd4c3c"}, - {file = "pybcj-1.0.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0275564a1afc4b2d1a6ff465384fb73a64622a88b6e4856cb7964ba2335a06e"}, - {file = "pybcj-1.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa794b134b4ee183a4ceb739e9c3a445a24ee12e7e3231c37820f66848db4c52"}, - {file = "pybcj-1.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d8945e8157c7fa469db110fc78579d154a31d121d14705b26d7d3ec3a471c8e"}, - {file = "pybcj-1.0.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7109177b4f77526a6ce4b565ee37483f5a5dd29bc92eaea6739b3c58618aeb7"}, - {file = "pybcj-1.0.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c48cbc9ebed137ac8759d0f2c3d12b999581dae7b4f84d974888c402f00fdb78"}, - {file = "pybcj-1.0.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6dccff82008e3cb5e5e639737320c02341b8718e189b9ece13f0230e0d57e7af"}, - {file = "pybcj-1.0.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e4e68cfc4fb099e8200386ac2255a9f514b8bb056189273bcce874bda3597459"}, - {file = "pybcj-1.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:13747c01b60bf955878267718f28c36e2bbb81fb8495b0173b21083c7d08a4a4"}, - {file = "pybcj-1.0.6-cp311-cp311-win_arm64.whl", hash = "sha256:6f81d6106c50c5e91c16ad58584fd7ab9eb941360188547e0184b1ede9e47f1d"}, - {file = "pybcj-1.0.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f5d1dbc76f615595d7d8f3846c07f607fb1e2305d085c34556b32dacf8e88d12"}, - {file = "pybcj-1.0.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1398f556ed2afe16ae363a2b6e8cf6aeda3aa21861757286bc6c498278886c60"}, - {file = "pybcj-1.0.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e269cfc7b6286af87c5447c9f8c685f19cff011cac64947ffb4cd98919696a7f"}, - {file = "pybcj-1.0.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7393d0b0dcaa0b1a7850245def78fa14438809e9a3f73b1057a975229d623fd3"}, - {file = "pybcj-1.0.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e252891698d3e01d0f60eb5adfe849038cd2d429cb9510f915a0759301f1884d"}, - {file = "pybcj-1.0.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ae5c891fcda9d5a6826a1b8e843b1e52811358594121553e6683e65b13eccce7"}, - {file = "pybcj-1.0.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:eac3cb317df1cefed2783ce9cafdae61899dd02f2f4749dc0f4494a7c425745f"}, - {file = "pybcj-1.0.6-cp312-cp312-win_amd64.whl", hash = "sha256:72ebec5cda5a48de169c2d7548ea2ce7f48732de0175d7e0e665ca7360eaa4c4"}, - {file = "pybcj-1.0.6-cp312-cp312-win_arm64.whl", hash = "sha256:8f1f75a01e45d01ecf88d31910ca1ace5d345e3bfb7c18db0af3d0c393209b63"}, - {file = "pybcj-1.0.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3e6800eb599ce766e588095eedb2a2c45a93928d1880420e8ecfad7eff0c73dc"}, - {file = "pybcj-1.0.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69a841ca0d3df978a2145488cec58460fa4604395321178ba421384cff26062f"}, - {file = "pybcj-1.0.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:887521da03302c96048803490073bd0423ff408a3adca2543c6ee86bc0af7578"}, - {file = "pybcj-1.0.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39a5a9a2d0e1fa4ddd2617a549c11e5022888af86dc8e29537cfee7f5761127d"}, - {file = "pybcj-1.0.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57757bc382f326bd93eb277a9edfc8dff6c22f480da467f0c5a5d63b9d092a41"}, - {file = "pybcj-1.0.6-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb1872b24b30d8473df433f3364e828b021964229d47a07f7bfc08496dbfd23e"}, - {file = "pybcj-1.0.6-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:5fedfeed96ab0e34207097f663b94e8c7076025c2c7af6a482e670e808ea5bb0"}, - {file = "pybcj-1.0.6-cp313-cp313-win_amd64.whl", hash = "sha256:caefc3109bf172ad37b52e21dc16c84cf495b2ea2890cc7256cdf0188914508d"}, - {file = "pybcj-1.0.6-cp313-cp313-win_arm64.whl", hash = "sha256:b24367175528da452a19e4c55368d5c907f4584072dc6aeee8990e2a5e6910fc"}, - {file = "pybcj-1.0.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:558128fbc201c9f11c1b1df30377fab3821ebb736c28e5eaf9fff9cc9e56b806"}, - {file = "pybcj-1.0.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d05f4026154d77c97486d5ce04261b473e3ec8c2f7cf0f937b7baa439c616559"}, - {file = "pybcj-1.0.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96ce9c428800ecc0d52cec9947ee167f3a7f913cc2ba58b9a462e7f19c52ac4b"}, - {file = "pybcj-1.0.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05038a58d78ab15a847ed90c17d924be5b7848f27a43517dc88a5589fba1ca78"}, - {file = "pybcj-1.0.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:591f58891ff52585a38894b28c8b952e4c7be93f65d6d43751672cde8edeff36"}, - {file = "pybcj-1.0.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1f416250101631ac04705a19d78ec407d261da9dffa0e1fa1f1f2d9409ec70d"}, - {file = "pybcj-1.0.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:27c489dd9e0d9745ebf7cd4344f23b6cb655edb2dea879ca63a0558a993e0d4b"}, - {file = "pybcj-1.0.6-cp39-cp39-win_amd64.whl", hash = "sha256:56fe3ff939653c6b0e35aa105170af3494ee9e2469494ef1d0fa2bac3fdd99d0"}, - {file = "pybcj-1.0.6-cp39-cp39-win_arm64.whl", hash = "sha256:6c88e1a04b90547f0470e4d2bd190bbe6b73c8666d4f7196c3ca43a379a15de5"}, - {file = "pybcj-1.0.6.tar.gz", hash = "sha256:70bbe2dc185993351955bfe8f61395038f96f5de92bb3a436acb01505781f8f2"}, + {file = "pybcj-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:618ec7345775c306d83527750e2d0ab3f42ffdc5ad6282f62f88cb53c9b2b679"}, + {file = "pybcj-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7e7faa1b0f7d894685e4567dd41268b93df89cff347ebfdfdc48b4bc0d68cb2"}, + {file = "pybcj-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3cd4b2d05272df605d5bdb54b3386985a2b074b4d97072da944736abd639fdee"}, + {file = "pybcj-1.0.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8eb5cd6f52df8857a8d9de594ca28a71683169b9de5af7e727c0e510aedb4550"}, + {file = "pybcj-1.0.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9d10760356b7d254b7b04ff38e052d5229c5f5a69a5514c9c31cb1dbb7d7f82"}, + {file = "pybcj-1.0.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1696d9b50971e317f72802bebd18f9b53684892ad3c43e0258f34e0a01738484"}, + {file = "pybcj-1.0.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e3d91b5dfdb0a200545b68145d81dae4edd2c385d89643dc45d6d01291f5c04"}, + {file = "pybcj-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:74df8f4c897f937105e8cd830df3b4ddf65ab5b5ba3e63cd6e3aeb3f4ecb0864"}, + {file = "pybcj-1.0.7-cp310-cp310-win_arm64.whl", hash = "sha256:dc121ecb26fdc1a4173a20b3c7cca5d8cc81494b485d4b44a62ed8448f8c796e"}, + {file = "pybcj-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:906ee707e89302253813a123f90a36d94d1f3c8785a4a1b853b31ac67296857a"}, + {file = "pybcj-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93da8503161fd51e01843aca031444fd46dce83e8a8bb4972f0256d6b3d280d3"}, + {file = "pybcj-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb4a52cd573f4359a89fd3a4a1d82c914f8b758a5c9f16cd5dd13fb8aa24436"}, + {file = "pybcj-1.0.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6d9a26fa9e627eb2fbba0f5b376ab42246bebdaf38cf437e384a6b7e3d78e23"}, + {file = "pybcj-1.0.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47207b69997fdc39e91a66812477506267964284b7d45fed68876dd74323d44f"}, + {file = "pybcj-1.0.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b33d6ef1de94720f4856e198bd2b8eca978015ed685aef4138755ba3910eb963"}, + {file = "pybcj-1.0.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:053c7cda499a8934151d0c915b6efce8e53fa6b47d162434a5b24afef7af5d17"}, + {file = "pybcj-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:555e90270d665d94cd34d2e50b096f68dba6baf7035ae11ac65c2bc126f8cef7"}, + {file = "pybcj-1.0.7-cp311-cp311-win_arm64.whl", hash = "sha256:22bdb390da9a4e38b2191070a62b88ad52edc3f6e12fe7eea278217ccfdbc02c"}, + {file = "pybcj-1.0.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d39787b85678d2ab1c67e2f21dd2e71be851f08e5c9fe619c605877b57dd529d"}, + {file = "pybcj-1.0.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8cd5dd166093a1fb146fb78859aac0f00b45db6c11074705517bc72a940a1c8e"}, + {file = "pybcj-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82152e8641f5ce68638f3504227065f27b6b1efe96479ffbf20d81530c220062"}, + {file = "pybcj-1.0.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2095b45d05f8d19430167b7df52ebd920df854ab8d064bae879df0a4611374b3"}, + {file = "pybcj-1.0.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b400c9f48faed01edb7f0df54b4354270325c886e785f31c866c581a46023b"}, + {file = "pybcj-1.0.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8210e51a2d4e5ccb4fdb75a1e692dd8c121858b589026bb28988ed7ffdb7ed00"}, + {file = "pybcj-1.0.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6c3fe420083186ae2e5f75c23aa6563dcb030b8fc188d00778ce374d1df1984"}, + {file = "pybcj-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:c435062d66364f85674a639541980000e37657b98367a2ce2699514e44b8ab05"}, + {file = "pybcj-1.0.7-cp312-cp312-win_arm64.whl", hash = "sha256:3f74fd70b08092e58b1ee13c67fbf9de63d73eb1c61ab06670a0d7161efeb252"}, + {file = "pybcj-1.0.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d5e0feeeee3a659b30d7afbd89bf41da84e8c8fe13e5b997457e799a70fa550"}, + {file = "pybcj-1.0.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:60baaf9f0da31438515a401145f920f75f2ec7d511165bbf57475467af72a3e6"}, + {file = "pybcj-1.0.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c6e726618c3d43c730df5a4067fc19653b360f89c2f72f4323dae10d324552"}, + {file = "pybcj-1.0.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a5fcd40a4ce8f0c5428032ec5db9f03abb42214b993886cdf558e5644de636e"}, + {file = "pybcj-1.0.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:029112255c22de66e0117bec932c8be341ed20c56dcf6a961c14689f7f0ce772"}, + {file = "pybcj-1.0.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6492bcef5cb6883506b9dce5e48cb81217407305957b0e602c6c689c60097c5e"}, + {file = "pybcj-1.0.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22a7f4a51d36a1abb67a61e93248f997eb2be278f788d681096f5044ae18b4f9"}, + {file = "pybcj-1.0.7-cp313-cp313-win_amd64.whl", hash = "sha256:ebcce9b419fe5d3109150a1fab0fc93a64d5cd812ca44c5ddb7d4f7128ea369f"}, + {file = "pybcj-1.0.7-cp313-cp313-win_arm64.whl", hash = "sha256:bc6acf0320976b4e31bdc0e59b16689083d5c346a6c62ac4f799685d1cc5cf27"}, + {file = "pybcj-1.0.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:293f951eb3877840acab79f0c4dcfc06eab03e087cb9e4c004ec058e093acb1d"}, + {file = "pybcj-1.0.7-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3ae64960904f362d33ffca10715803afd9f9a6a2a592f871dcb335acf82edf29"}, + {file = "pybcj-1.0.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:70aa4476910f982025f878e598c136559a6d78b59fc20ba8b4b592306cde6051"}, + {file = "pybcj-1.0.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79ce3ce9b380b1b75c5e490abc3888ee3b5b2d28c22b59618674bf410b9cee16"}, + {file = "pybcj-1.0.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc73ee1bc064d6f97dfd66051d3859b32e1b6a4cf89b077f5c8ef6c2dccb71af"}, + {file = "pybcj-1.0.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5b0d13f41a9f85b3f95dd5dc7bfaa9539e80f8ae60a96db7f34c07ed732e4a82"}, + {file = "pybcj-1.0.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:597d7e9a8cbb30a6ed54d552fd3436edb32bbb821a7ac2fa8e5c7ebd1f7e0e93"}, + {file = "pybcj-1.0.7-cp314-cp314-win_amd64.whl", hash = "sha256:4603cc41ceb1236abe9169e2ead344140be5d2c3ac01bbc5e44cb1b13078a009"}, + {file = "pybcj-1.0.7-cp314-cp314-win_arm64.whl", hash = "sha256:adf985e816ddd59f3bf6d1066b7fa89de7424a4f19f3725f9976284cabe54e28"}, + {file = "pybcj-1.0.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9bbd835873de147481d62c11ba91a75d26a72df1142de3516b384b04e5a1db6d"}, + {file = "pybcj-1.0.7-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b7576b25d7b01a953e2f987e77cef93c001db7b95924a5541d5a55f9195a7e89"}, + {file = "pybcj-1.0.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57b36920498f82ca6a325a98b13e0fbff8fc29bade7aaaddc7d284640bffd87d"}, + {file = "pybcj-1.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aac2a46faf41e373939f6d3e6a5aa2121bf09e2446972c14a8e5d1ca3b0f8130"}, + {file = "pybcj-1.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c7d6156ef2b4e8ecd450b62dc4cc3a89e8dda307cb26288b670952ef0df3a37"}, + {file = "pybcj-1.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0fe306213de1e764abae63c06ae5a4e9a83632f62612805f1f883b8d74431901"}, + {file = "pybcj-1.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00448182d535cca37e8f24d892d480fa86f80ff20c79385f6eca75f118efcbb4"}, + {file = "pybcj-1.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:7e94aa712d0fa5fda9875828441755ece7121fc3f8c5cc3bc8ee92d05b853590"}, + {file = "pybcj-1.0.7-cp314-cp314t-win_arm64.whl", hash = "sha256:16fd4e51a5556d1f38d7ba5d1fab588bfb60ae23d2299b5179779bf9900adf71"}, + {file = "pybcj-1.0.7.tar.gz", hash = "sha256:72d64574069ffb0a800020668376b7ebd7adea159adbf4d35f8effc62f0daa67"}, ] [package.extras] -check = ["check-manifest", "flake8 (<5)", "flake8-black", "flake8-colors", "flake8-isort", "flake8-pyi", "flake8-typing-imports", "mypy (>=1.10.0)", "pygments", "readme-renderer"] +check = ["check-manifest", "flake8 (<8)", "flake8-black", "flake8-colors", "flake8-isort", "flake8-pyi", "flake8-typing-imports", "mypy (>=1.10.0)", "pygments", "readme-renderer"] test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-cov"] [[package]] @@ -1337,15 +1744,15 @@ files = [ [[package]] name = "pycparser" -version = "2.22" +version = "3.0" description = "C parser in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["tutorials"] -markers = "platform_python_implementation == \"PyPy\"" +markers = "implementation_name != \"PyPy\" and platform_python_implementation == \"PyPy\"" files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] [[package]] @@ -1428,67 +1835,73 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyppmd" -version = "1.1.1" +version = "1.3.1" description = "PPMd compression/decompression library" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["tutorials"] files = [ - {file = "pyppmd-1.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:406b184132c69e3f60ea9621b69eaa0c5494e83f82c307b3acce7b86a4f8f888"}, - {file = "pyppmd-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2cf003bb184adf306e1ac1828107307927737dde63474715ba16462e266cbef"}, - {file = "pyppmd-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71c8fd0ecc8d4760e852dd6df19d1a827427cb9e6c9e568cbf5edba7d860c514"}, - {file = "pyppmd-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6b5edee08b66ad6c39fd4d34a7ef4cfeb4b69fd6d68957e59cd2db674611a9e"}, - {file = "pyppmd-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e95bd23eb1543ab3149f24fe02f6dd2695023326027a4b989fb2c6dba256e75e"}, - {file = "pyppmd-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e633ee4cc19d0c71b3898092c3c4cc20a10bd5e6197229fffac29d68ad5d83b8"}, - {file = "pyppmd-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ecaafe2807ef557f0c49b8476a4fa04091b43866072fbcf31b3ceb01a96c9168"}, - {file = "pyppmd-1.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c182fccff60ae8f24f28f5145c36a60708b5b041a25d36b67f23c44923552fa4"}, - {file = "pyppmd-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:70c93d19efe67cdac3e7fa2d4e171650a2c4f90127a9781b25e496a43f12fbbc"}, - {file = "pyppmd-1.1.1-cp310-cp310-win32.whl", hash = "sha256:57c75856920a210ed72b553885af7bc06eddfd30ff26b62a3a63cb8f86f3d217"}, - {file = "pyppmd-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d5293f10dc8c1d571b780e0d54426d3d858c19bbd8cb0fe972dcea3906acd05c"}, - {file = "pyppmd-1.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:753c5297c91c059443caef33bccbffb10764221739d218046981638aeb9bc5f2"}, - {file = "pyppmd-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b5a73da09de480a94793c9064876af14a01be117de872737935ac447b7cde3c"}, - {file = "pyppmd-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89c6febb7114dea02a061143d78d04751a945dfcadff77560e9a3d3c7583c24b"}, - {file = "pyppmd-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0001e467c35e35e6076a8c32ed9074aa45833615ee16115de9282d5c0985a1d8"}, - {file = "pyppmd-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c76820db25596afc859336ba06c01c9be0ff326480beec9c699fd378a546a77f"}, - {file = "pyppmd-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b67f0a228f8c58750a21ba667c170ae957283e08fd580857f13cb686334e5b3e"}, - {file = "pyppmd-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b18f24c14f0b0f1757a42c458ae7b6fd7aa0bce8147ac1016a9c134068c1ccc2"}, - {file = "pyppmd-1.1.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c9e43729161cc3b6ad5b04b16bae7665d3c0cc803de047d8a979aa9232a4f94a"}, - {file = "pyppmd-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fe057d254528b4eeebe2800baefde47d6af679bae184d3793c13a06f794df442"}, - {file = "pyppmd-1.1.1-cp311-cp311-win32.whl", hash = "sha256:faa51240493a5c53c9b544c99722f70303eea702742bf90f3c3064144342da4a"}, - {file = "pyppmd-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:62486f544d6957e1381147e3961eee647b7f4421795be4fb4f1e29d52aee6cb5"}, - {file = "pyppmd-1.1.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9877ef273e2c0efdec740855e28004a708ada9012e0db6673df4bb6eba3b05e0"}, - {file = "pyppmd-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f816a5cbccceced80e15335389eeeaf1b56a605fb7eebe135b1c85bd161e288c"}, - {file = "pyppmd-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6bddabf8f2c6b991d15d6785e603d9d414ae4a791f131b1a729bb8a5d31133d1"}, - {file = "pyppmd-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:855bc2b0d19c3fead5815d72dbe350b4f765334336cbf8bcb504d46edc9e9dd2"}, - {file = "pyppmd-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a95b11b3717c083b912f0879678ba72f301bbdb9b69efed46dbc5df682aa3ce7"}, - {file = "pyppmd-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38b645347b6ea217b0c58e8edac27473802868f152db520344ac8c7490981849"}, - {file = "pyppmd-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f8f94b6222262def5b532f2b9716554ef249ad8411fd4da303596cc8c2e8eda1"}, - {file = "pyppmd-1.1.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1c0306f69ceddf385ef689ebd0218325b7e523c48333d87157b37393466cfa1e"}, - {file = "pyppmd-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4ba510457a56535522a660098399e3fa8722e4de55808d089c9d13435d87069"}, - {file = "pyppmd-1.1.1-cp312-cp312-win32.whl", hash = "sha256:032f040a89fd8348109e8638f94311bd4c3c693fb4cad213ad06a37c203690b1"}, - {file = "pyppmd-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:2be8cbd13dd59fad1a0ad38062809e28596f3673b77a799dfe82b287986265ed"}, - {file = "pyppmd-1.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9458f972f090f3846fc5bea0a6f7363da773d3c4b2d4654f1d4ca3c11f6ecbfa"}, - {file = "pyppmd-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:44811a9d958873d857ca81cebf7ba646a0952f8a7bbf8a60cf6ec5d002faa040"}, - {file = "pyppmd-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a1b12460958885ca44e433986644009d0599b87a444f668ce3724a46ce588924"}, - {file = "pyppmd-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:200c74f05b97b00f047cf60607914a0b50f80991f1fb3677f624a85aa79d9458"}, - {file = "pyppmd-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ebe0d98a341b32f164e860059243e125398865cc0363b32ffc31f953460fe87"}, - {file = "pyppmd-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf93e1e047a82f1e7e194fcf49da166d2b9d8dc98d7c0b5cd844dc4360d9c1f5"}, - {file = "pyppmd-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f5b0b8c746bde378ae3b4df42a11fd8599ba3e5808dfea36e16d722b74bd0506"}, - {file = "pyppmd-1.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bcdd5207b6c79887f25639632ca2623a399d8c54f567973e9ba474b5ebae2b1c"}, - {file = "pyppmd-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7bfcca94e5452b6d54ac24a11c2402f6a193c331e5dc221c1f1df71773624374"}, - {file = "pyppmd-1.1.1-cp39-cp39-win32.whl", hash = "sha256:18e99c074664f996f511bc6e87aab46bc4c75f5bd0157d3210292919be35e22c"}, - {file = "pyppmd-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b29788d5a0f8f39ea46a1255cd886daddf9c64ba9d4cb64677bc93bd3859ac0e"}, - {file = "pyppmd-1.1.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28648ef56793bf1ed0ff24728642f56fa39cb96ea161dec6ee2d26f97c0cdd28"}, - {file = "pyppmd-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:427d6f9b9c011e032db9529b2a15773f2e2944ca490b67d5757f4af33bbda406"}, - {file = "pyppmd-1.1.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34c7a07197a03656c1920fd88e05049c155a955c4de4b8b8a8e5fec19a97b45b"}, - {file = "pyppmd-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1fea2eee28beca61165c4714dcd032de76af318553791107d308b4b08575ecc"}, - {file = "pyppmd-1.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:04391e4f82c8c2c316ba60e480300ad1af37ec12bdb5c20f06b502030ff35975"}, - {file = "pyppmd-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cf08a354864c352a94e6e53733009baeab1e7c570010c4f5be226923ecfa09d1"}, - {file = "pyppmd-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:334e5fe5d75764b87c591a16d2b2df6f9939e2ad114dacf98bb4b0e7c90911e9"}, - {file = "pyppmd-1.1.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d5928b25f04f5431585d17c835cd509a34e1c9f1416653db8d2815e97d4e20"}, - {file = "pyppmd-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af06329796a4965788910ac40f1b012d2e173ede08456ceea0ec7fc4d2e69d62"}, - {file = "pyppmd-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4ccdd3751e432e71e02de96f16fc8824e4f4bfc47a8b470f0c7aae88dae4c666"}, - {file = "pyppmd-1.1.1.tar.gz", hash = "sha256:f1a812f1e7628f4c26d05de340b91b72165d7b62778c27d322b82ce2e8ff00cb"}, + {file = "pyppmd-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:041f46fbeb0a59888c0a94d6b9a557c652935633a104be1c31c12de491b5f448"}, + {file = "pyppmd-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9512a8b39740923559c26eb16266bf8b70d4eab6ad27a9b39cd2465e60e0acfa"}, + {file = "pyppmd-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8966f26b91ba7cdff3cfec5512d39d1f8bf4a8dbb75c44085e33b564566fea66"}, + {file = "pyppmd-1.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d1cff657e85655c67426c29c90c78a6210148b207993e643fc351c72c60d188"}, + {file = "pyppmd-1.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9de2cdcc3932e7c23a54beb48dfe1b5ab7b4aedd5ffaae1e4871bd213d630cb3"}, + {file = "pyppmd-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e1985461219c30d4576070b7e2de718dbb6f32637d1e658d25f838dfda2a4bb"}, + {file = "pyppmd-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20d9d1aa4d0f32118c8094c212c66b7af50e55f47e7c6dffa5f35a8ac391faca"}, + {file = "pyppmd-1.3.1-cp310-cp310-win32.whl", hash = "sha256:44d25e7dede2abb614bc023fe87835365fdd5865981c2273b70bfad71b84db29"}, + {file = "pyppmd-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:1e503a28c9a275d31f24af9b735d2cca543b62f438b064e2833e9833e758bdbc"}, + {file = "pyppmd-1.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:3fb3708d7b2b38e2999385a2f02c8e68e0f5a364d94f94e475e2e8b09e9338fc"}, + {file = "pyppmd-1.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdf55aa6ee7aef492f6896464e7a5a528f8615bb9e435f55bc8dff226fcc8292"}, + {file = "pyppmd-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa820aac385ac4ee57160b26d92862c69d31c08f92272dbef05fe8e619cea8d1"}, + {file = "pyppmd-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e16593ba4ca0a85821ae698ef06847a52937662f5ce1b130c39cca2979a4e8cd"}, + {file = "pyppmd-1.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:486dde2294ff9b30465ab5bb0f213b20bd5ac0e4adf21be801a1ceb29aa75d9d"}, + {file = "pyppmd-1.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89730cf026416ae2546c92738966ecf117c8176d52c229ad621a61c34643818b"}, + {file = "pyppmd-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e77f5a6770950d464b50da760d53e67bce308a3abc8e3bd51db620b3f8cf1fa8"}, + {file = "pyppmd-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8e3bf8deef44f8e03612689a6067a4a3dd7e50d2ef00af4cf987c59b62ff3006"}, + {file = "pyppmd-1.3.1-cp311-cp311-win32.whl", hash = "sha256:a3509b3f881409ebc5522942438108c48a78f8df88bcf3f9d907b74131b9431c"}, + {file = "pyppmd-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:23b83799f33f9a24577f22e092b0feecda8cd1ea33871ad8610a58629874f7bc"}, + {file = "pyppmd-1.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:234489036a1758670655d1ceafd4caeb93b858bd4c0ca39686837d38aef044c0"}, + {file = "pyppmd-1.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3faa58ab2ebe3b13ec23b1904639d687fb727270d2962fd2d239ca00fd6eb865"}, + {file = "pyppmd-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27703f041ee96912a5410fd3ce31c5cde32f9323bd67f72f100bd960ee67bf13"}, + {file = "pyppmd-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e773d8353b36f7e7973a43526993fb276b98a97839cb5dc8f4e6465ad873f41a"}, + {file = "pyppmd-1.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37b1883accf840cb0b711785d353f8548853a1401d381da007c0aec362f3ffac"}, + {file = "pyppmd-1.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bd6d179ad39b6191ca0cbe62fb9592f33f49277b4384ad7bc5eb0e6ca27ebee"}, + {file = "pyppmd-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:806cf8d33606e44bf5ff5786c57891f57993f1eef1c763da3c58ea97de3a13c8"}, + {file = "pyppmd-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1826cbce9a2c944aa08df79310a7e6d4a61fd20636b6dff64a77ea4bc43da30f"}, + {file = "pyppmd-1.3.1-cp312-cp312-win32.whl", hash = "sha256:d3ff96671319318d941dd34300d641745048e8a3251b077bddf98652d6ddc513"}, + {file = "pyppmd-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:c8c1ad39e7ebde71bf5a54cf61f489bf4790f1dd0beb70dc2e8f5ad3329d7ca7"}, + {file = "pyppmd-1.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:391b2bf76d7dc45b343781754d0b734dcbf539b92667986a343f5488c4bf9ca0"}, + {file = "pyppmd-1.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4b4edb3e9619fd0bc39c1a07eb03e8731db833a93b23134f36c7ef581a94b37a"}, + {file = "pyppmd-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8b5c813e462c91048b88e2adfbcc0c69f2c905f70097001d32066f86f675bd4"}, + {file = "pyppmd-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e8d372d9fac382183e0371cf0c2d736b494b1857a1befe98d563342b1205265b"}, + {file = "pyppmd-1.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b765ae21f7ed2f4ea8f32bfd9e3a4a8d738e73fc8f8dcddec9cbe2c898d60be"}, + {file = "pyppmd-1.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd00522ddfcc292304577386b6c217758c0c10e1fb9ce7877ad7d3b7b821a808"}, + {file = "pyppmd-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3de62099ff2ca876c2d39bc547bcba6f7b878988663abd782a5bad4edac3bb44"}, + {file = "pyppmd-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:011f845de195d60fe973a635a1f4be981b7d80f357a8acb1b2d83bdf5087c808"}, + {file = "pyppmd-1.3.1-cp313-cp313-win32.whl", hash = "sha256:7d61bd01f25289b6ae54832db4254602fb0c6d105f6e6bf0aee39b803b698b98"}, + {file = "pyppmd-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:df8d84ab72381058a964ba66e5e81ed52dbd0b5ad734a5ef8353452983506098"}, + {file = "pyppmd-1.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:124a04aab6936ba011f9ad57067798c7f052fdb1848b0cc4318606eea55475e6"}, + {file = "pyppmd-1.3.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ea53f71ac16e113599b8441a9d8b6dcd71cfdf15cdb33ba5151810b8e656c5ec"}, + {file = "pyppmd-1.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:985c8703b53e5f68fe17f653e96748d60b1f855676c852a6e67cd472eb853671"}, + {file = "pyppmd-1.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:33daa996ad5203c665c0b55aff6329817b2cb7fa95f2c33a2e83ed0121b400cb"}, + {file = "pyppmd-1.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b49870c6d7194f6eb80f30335ca03596d153e02fcde2c222e4f1202ac25f7fcf"}, + {file = "pyppmd-1.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:385e92c97c42e8a6f0bfc0e4acfc6c074cb1ba3a2f650f292696dd9f19e2e603"}, + {file = "pyppmd-1.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:017a1e2903f1c3147a1046db486990d401e8a25eb52c320b1fc2fb3e7b83cbeb"}, + {file = "pyppmd-1.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f2770a4b777c0c5236b3d9294b7bf4bc15538c95d45b2079eb8ebc1298e62e37"}, + {file = "pyppmd-1.3.1-cp314-cp314-win32.whl", hash = "sha256:b9d54cd59ce97f2ba57be1da91b3d874d129faca21c9565d7afec111f942e6a1"}, + {file = "pyppmd-1.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0e64247618cb150d2909beb0137da3084fef1d3479b4cc73b5b47fda7611abf9"}, + {file = "pyppmd-1.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:d354d6e551d2630b0ac98f27e3ad63e86cdcac9ce2115b5dfe46e2c9d3f4e82c"}, + {file = "pyppmd-1.3.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2d77ed79662c32e2551748d59763cfe3dcd10855bf3495937e3d5e5917507818"}, + {file = "pyppmd-1.3.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6a1f92b94635c23d85270bb26db25cc0db544e436af86efc1cf58302d71d5af1"}, + {file = "pyppmd-1.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:610f214f2405e27eb5a3dad7fa15f385cfc42141a01cda71995d9c1e0b09fab9"}, + {file = "pyppmd-1.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae81a14895498d9a23429d92114c98da478b74b8e33251527d7cff3e01c09de0"}, + {file = "pyppmd-1.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1683e6d1ba09e377e0ae02de3a518191a3d63ccdb0b6037c74e6ddf577b5644"}, + {file = "pyppmd-1.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e4d74fa0f3531e9dadc56e0ace41bce82d3c0babed47b3f224101dc0dbde7287"}, + {file = "pyppmd-1.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f8d6375b18b9c79127fee0885cfd52e2e983edb67041464309426571d38dcd4"}, + {file = "pyppmd-1.3.1-cp314-cp314t-win32.whl", hash = "sha256:de87f7acd575fb07a4ff42d41bcc071570fe759a36f345f1f54f574ecfccfc5b"}, + {file = "pyppmd-1.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:76e4800aa67292b4cc80058fd29b39e02a5dded721af9fe5654f356ef24307f4"}, + {file = "pyppmd-1.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:e066cbf1d335fe20480cd8ecc10848ba78d99fe6d1e44ea00def48feaf46afdf"}, + {file = "pyppmd-1.3.1.tar.gz", hash = "sha256:ced527f08ade4408c1bfc5264e9f97ffac8d221c9d13eca4f35ec1ec0c7b6b2e"}, ] [package.extras] @@ -1499,30 +1912,32 @@ test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-benchm [[package]] name = "pyspark" -version = "4.0.0" +version = "4.1.2" description = "Apache Spark Python API" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pyspark-4.0.0.tar.gz", hash = "sha256:38db1b4f6095a080d7605e578d775528990e66dc326311d93e94a71cfc24e5a5"}, + {file = "pyspark-4.1.2.tar.gz", hash = "sha256:fa5d6159f700d0990a07f4f62df1b7449401dccee9cd7d5d6df8957530841602"}, ] [package.dependencies] -googleapis-common-protos = {version = ">=1.65.0", optional = true, markers = "extra == \"connect\""} -grpcio = {version = ">=1.67.0", optional = true, markers = "extra == \"connect\""} -grpcio-status = {version = ">=1.67.0", optional = true, markers = "extra == \"connect\""} +googleapis-common-protos = {version = ">=1.71.0", optional = true, markers = "extra == \"connect\""} +grpcio = {version = ">=1.76.0", optional = true, markers = "extra == \"connect\""} +grpcio-status = {version = ">=1.76.0", optional = true, markers = "extra == \"connect\""} numpy = {version = ">=1.21", optional = true, markers = "extra == \"connect\""} -pandas = {version = ">=2.0.0", optional = true, markers = "extra == \"connect\""} -py4j = "0.10.9.9" -pyarrow = {version = ">=11.0.0", optional = true, markers = "extra == \"connect\""} +pandas = {version = ">=2.2.0", optional = true, markers = "extra == \"connect\""} +py4j = ">=0.10.9.7,<0.10.9.10" +pyarrow = {version = ">=15.0.0", optional = true, markers = "extra == \"connect\""} +zstandard = {version = ">=0.25.0", optional = true, markers = "extra == \"connect\""} [package.extras] -connect = ["googleapis-common-protos (>=1.65.0)", "grpcio (>=1.67.0)", "grpcio-status (>=1.67.0)", "numpy (>=1.21)", "pandas (>=2.0.0)", "pyarrow (>=11.0.0)"] +connect = ["googleapis-common-protos (>=1.71.0)", "grpcio (>=1.76.0)", "grpcio-status (>=1.76.0)", "numpy (>=1.21)", "pandas (>=2.2.0)", "pyarrow (>=15.0.0)", "zstandard (>=0.25.0)"] ml = ["numpy (>=1.21)"] mllib = ["numpy (>=1.21)"] -pandas-on-spark = ["numpy (>=1.21)", "pandas (>=2.0.0)", "pyarrow (>=11.0.0)"] -sql = ["numpy (>=1.21)", "pandas (>=2.0.0)", "pyarrow (>=11.0.0)"] +pandas-on-spark = ["numpy (>=1.21)", "pandas (>=2.2.0)", "pyarrow (>=15.0.0)"] +pipelines = ["googleapis-common-protos (>=1.71.0)", "grpcio (>=1.76.0)", "grpcio-status (>=1.76.0)", "numpy (>=1.21)", "pandas (>=2.2.0)", "pyarrow (>=15.0.0)", "pyyaml (>=3.11)", "zstandard (>=0.25.0)"] +sql = ["numpy (>=1.21)", "pandas (>=2.2.0)", "pyarrow (>=15.0.0)"] [[package]] name = "pyspark-client" @@ -1547,14 +1962,14 @@ zstandard = ">=0.25.0" [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["connect", "dev"] files = [ - {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, - {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, ] [package.dependencies] @@ -1586,14 +2001,15 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2025.2" +version = "2026.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["connect", "dev"] +markers = "python_version == \"3.10\"" files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, + {file = "pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126"}, + {file = "pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a"}, ] [[package]] @@ -1679,123 +2095,16 @@ files = [ {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] -[[package]] -name = "pyzstd" -version = "0.17.0" -description = "Python bindings to Zstandard (zstd) compression library." -optional = false -python-versions = ">=3.5" -groups = ["tutorials"] -files = [ - {file = "pyzstd-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ac857abb4c4daea71f134e74af7fe16bcfeec40911d13cf9128ddc600d46d92"}, - {file = "pyzstd-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d84e8d1cbecd3b661febf5ca8ce12c5e112cfeb8401ceedfb84ab44365298ac"}, - {file = "pyzstd-0.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f829fa1e7daac2e45b46656bdee13923150f329e53554aeaef75cceec706dd8c"}, - {file = "pyzstd-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:994de7a13bb683c190a1b2a0fb99fe0c542126946f0345360582d7d5e8ce8cda"}, - {file = "pyzstd-0.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3eb213a22823e2155aa252d9093c62ac12d7a9d698a4b37c5613f99cb9de327"}, - {file = "pyzstd-0.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c451cfa31e70860334cc7dffe46e5178de1756642d972bc3a570fc6768673868"}, - {file = "pyzstd-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d66dc6f15249625e537ea4e5e64c195f50182556c3731f260b13c775b7888d6b"}, - {file = "pyzstd-0.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:308d4888083913fac2b7b6f4a88f67c0773d66db37e6060971c3f173cfa92d1e"}, - {file = "pyzstd-0.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a3b636f37af9de52efb7dd2d2f15deaeabdeeacf8e69c29bf3e7e731931e6d66"}, - {file = "pyzstd-0.17.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4c07391c67b496d851b18aa29ff552a552438187900965df57f64d5cf2100c40"}, - {file = "pyzstd-0.17.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e8bd12a13313ffa27347d7abe20840dcd2092852ab835a8e86008f38f11bd5ac"}, - {file = "pyzstd-0.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e27bfab45f9cdab0c336c747f493a00680a52a018a8bb7a1f787ddde4b29410"}, - {file = "pyzstd-0.17.0-cp310-cp310-win32.whl", hash = "sha256:7370c0978edfcb679419f43ec504c128463858a7ea78cf6d0538c39dfb36fce3"}, - {file = "pyzstd-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:564f7aa66cda4acd9b2a8461ff0c6a6e39a977be3e2e7317411a9f7860d7338d"}, - {file = "pyzstd-0.17.0-cp310-cp310-win_arm64.whl", hash = "sha256:fccff3a37fa4c513fe1ebf94cb9dc0369c714da22b5671f78ddcbc7ec8f581cc"}, - {file = "pyzstd-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06d1e7afafe86b90f3d763f83d2f6b6a437a8d75119fe1ff52b955eb9df04eaa"}, - {file = "pyzstd-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc827657f644e4510211b49f5dab6b04913216bc316206d98f9a75214361f16e"}, - {file = "pyzstd-0.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecffadaa2ee516ecea3e432ebf45348fa8c360017f03b88800dd312d62ecb063"}, - {file = "pyzstd-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:596de361948d3aad98a837c98fcee4598e51b608f7e0912e0e725f82e013f00f"}, - {file = "pyzstd-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd3a8d0389c103e93853bf794b9a35ac5d0d11ca3e7e9f87e3305a10f6dfa6b2"}, - {file = "pyzstd-0.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1356f72c7b8bb99b942d582b61d1a93c5065e66b6df3914dac9f2823136c3228"}, - {file = "pyzstd-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f514c339b013b0b0a2ed8ea6e44684524223bd043267d7644d7c3a70e74a0dd"}, - {file = "pyzstd-0.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4de16306821021c2d82a45454b612e2a8683d99bfb98cff51a883af9334bea0"}, - {file = "pyzstd-0.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aeb9759c04b6a45c1b56be21efb0a738e49b0b75c4d096a38707497a7ff2be82"}, - {file = "pyzstd-0.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a5b31ddeada0027e67464d99f09167cf08bab5f346c3c628b2d3c84e35e239a"}, - {file = "pyzstd-0.17.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8338e4e91c52af839abcf32f1f65f3b21e2597ffe411609bdbdaf10274991bd0"}, - {file = "pyzstd-0.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628e93862feb372b4700085ec4d1d389f1283ac31900af29591ae01019910ff3"}, - {file = "pyzstd-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c27773f9c95ebc891cfcf1ef282584d38cde0a96cb8d64127953ad752592d3d7"}, - {file = "pyzstd-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:c043a5766e00a2b7844705c8fa4563b7c195987120afee8f4cf594ecddf7e9ac"}, - {file = "pyzstd-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:efd371e41153ef55bf51f97e1ce4c1c0b05ceb59ed1d8972fc9aa1e9b20a790f"}, - {file = "pyzstd-0.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ac330fc4f64f97a411b6f3fc179d2fe3050b86b79140e75a9a6dd9d6d82087f"}, - {file = "pyzstd-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:725180c0c4eb2e643b7048ebfb45ddf43585b740535907f70ff6088f5eda5096"}, - {file = "pyzstd-0.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c20fe0a60019685fa1f7137cb284f09e3f64680a503d9c0d50be4dd0a3dc5ec"}, - {file = "pyzstd-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d97f7aaadc3b6e2f8e51bfa6aa203ead9c579db36d66602382534afaf296d0db"}, - {file = "pyzstd-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42dcb34c5759b59721997036ff2d94210515d3ef47a9de84814f1c51a1e07e8a"}, - {file = "pyzstd-0.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6bf05e18be6f6c003c7129e2878cffd76fcbebda4e7ebd7774e34ae140426cbf"}, - {file = "pyzstd-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40f7c3a5144aa4fbccf37c30411f6b1db4c0f2cb6ad4df470b37929bffe6ca0"}, - {file = "pyzstd-0.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9efd4007f8369fd0890701a4fc77952a0a8c4cb3bd30f362a78a1adfb3c53c12"}, - {file = "pyzstd-0.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5f8add139b5fd23b95daa844ca13118197f85bd35ce7507e92fcdce66286cc34"}, - {file = "pyzstd-0.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:259a60e8ce9460367dcb4b34d8b66e44ca3d8c9c30d53ed59ae7037622b3bfc7"}, - {file = "pyzstd-0.17.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:86011a93cc3455c5d2e35988feacffbf2fa106812a48e17eb32c2a52d25a95b3"}, - {file = "pyzstd-0.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:425c31bc3de80313054e600398e4f1bd229ee61327896d5d015e2cd0283c9012"}, - {file = "pyzstd-0.17.0-cp312-cp312-win32.whl", hash = "sha256:7c4b88183bb36eb2cebbc0352e6e9fe8e2d594f15859ae1ef13b63ebc58be158"}, - {file = "pyzstd-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c31947e0120468342d74e0fa936d43f7e1dad66a2262f939735715aa6c730e8"}, - {file = "pyzstd-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:1d0346418abcef11507356a31bef5470520f6a5a786d4e2c69109408361b1020"}, - {file = "pyzstd-0.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6cd1a1d37a7abe9c01d180dad699e3ac3889e4f48ac5dcca145cc46b04e9abd2"}, - {file = "pyzstd-0.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a44fd596eda06b6265dc0358d5b309715a93f8e96e8a4b5292c2fe0e14575b3"}, - {file = "pyzstd-0.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a99b37453f92f0691b2454d0905bbf2f430522612f6f12bbc81133ad947eb97"}, - {file = "pyzstd-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63d864e9f9e624a466070a121ace9d9cbf579eac4ed575dee3b203ab1b3cbeee"}, - {file = "pyzstd-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e58bc02b055f96d1f83c791dd197d8c80253275a56cd84f917a006e9f528420d"}, - {file = "pyzstd-0.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e62df7c0ba74618481149c849bc3ed7d551b9147e1274b4b3170bbcc0bfcc0a"}, - {file = "pyzstd-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42ecdd7136294f1becb8e57441df00eaa6dfd7444a8b0c96a1dfba5c81b066e7"}, - {file = "pyzstd-0.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:be07a57af75f99fc39b8e2d35f8fb823ecd7ef099cd1f6203829a5094a991ae2"}, - {file = "pyzstd-0.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0d41e6f7ec2a70dab4982157a099562de35a6735c890945b4cebb12fb7eb0be0"}, - {file = "pyzstd-0.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f482d906426756e7cc9a43f500fee907e1b3b4e9c04d42d58fb1918c6758759b"}, - {file = "pyzstd-0.17.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:827327b35605265e1d05a2f6100244415e8f2728bb75c951736c9288415908d7"}, - {file = "pyzstd-0.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a55008f80e3390e4f37bd9353830f1675f271d13d6368d2f1dc413b7c6022b3"}, - {file = "pyzstd-0.17.0-cp313-cp313-win32.whl", hash = "sha256:a4be186c0df86d4d95091c759a06582654f2b93690503b1c24d77f537d0cf5d0"}, - {file = "pyzstd-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:251a0b599bd224ec66f39165ddb2f959d0a523938e3bbfa82d8188dc03a271a2"}, - {file = "pyzstd-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:ce6d5fd908fd3ddec32d1c1a5a7a15b9d7737d0ef2ab20fe1e8261da61395017"}, - {file = "pyzstd-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5cb23c3c4ba4105a518cfbe8a566f9482da26f4bc8c1c865fd66e8e266be071"}, - {file = "pyzstd-0.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10b5d9215890a24f22505b68add26beeb2e3858bbe738a7ee339f0db8e29d033"}, - {file = "pyzstd-0.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db1cff52fd24caf42a2cfb7e5d8dc822b93e9fac5dab505d0bd22e302061e2d2"}, - {file = "pyzstd-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3caad3106e0e80f76acbb19c15e1e834ba6fd44dd4c82719ef8e3374f7fafd3"}, - {file = "pyzstd-0.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7e52e1de31b935e27568742145d8b4d0f204a1605e36f4e1e2846e0d39bed98"}, - {file = "pyzstd-0.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaa046bc9e751c4083102f3624a52bbb66e20e7aa3e28673543b22e69d9b57cd"}, - {file = "pyzstd-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cc9310bdb7cf2c70098aab40fb6bf68faaf0149110c6ef668996e7957e0147a"}, - {file = "pyzstd-0.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3619075966456783818904f9d9e213c6fe2e583d5beb545fa1968b1848781e0f"}, - {file = "pyzstd-0.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3844f8c7d7850580423b1b33601b016b3b913d18deb6fe14a7641b9c2714275c"}, - {file = "pyzstd-0.17.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab53f91280b7b639c47bb2048e01182230e7cf3f0f0980bdb405b4241cfb705e"}, - {file = "pyzstd-0.17.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:75252ee53e53a819ea7ac4271f66686018bc8b98ef12628269f099c10d881077"}, - {file = "pyzstd-0.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0795afdaa34e1ed7f3d7552100cd57a1cef9d7310b386a893e0890e9a585b427"}, - {file = "pyzstd-0.17.0-cp39-cp39-win32.whl", hash = "sha256:f7316be5a5246b6bbdd807c7a4f10382b6b02c3afc5ae6acd2e266a84f715493"}, - {file = "pyzstd-0.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:121e8fac3e24b881fed59d638100b80c34f6347c02d2f24580f633451939f2d7"}, - {file = "pyzstd-0.17.0-cp39-cp39-win_arm64.whl", hash = "sha256:fe36ccda67f73e909ac305984fe13b7b5a79296706d095a80472ada4413174c2"}, - {file = "pyzstd-0.17.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c56f99c697130f39702e07ab9fa0bb4c929c7bfe47c0a488dea732bd8a8752a"}, - {file = "pyzstd-0.17.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:152bae1b2197bcd41fc143f93acd23d474f590162547484ca04ce5874c4847de"}, - {file = "pyzstd-0.17.0-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2ddbbd7614922e52018ba3e7bb4cbe6f25b230096831d97916b8b89be8cd0cb"}, - {file = "pyzstd-0.17.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f6f3f152888825f71fd2cf2499f093fac252a5c1fa15ab8747110b3dc095f6b"}, - {file = "pyzstd-0.17.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d00a2d2bddf51c7bf32c17dc47f0f49f47ebae07c2528b9ee8abf1f318ac193"}, - {file = "pyzstd-0.17.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d79e3eff07217707a92c1a6a9841c2466bfcca4d00fea0bea968f4034c27a256"}, - {file = "pyzstd-0.17.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3ce6bac0c4c032c5200647992a8efcb9801c918633ebe11cceba946afea152d9"}, - {file = "pyzstd-0.17.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:a00998144b35be7c485a383f739fe0843a784cd96c3f1f2f53f1a249545ce49a"}, - {file = "pyzstd-0.17.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8521d7bbd00e0e1c1fd222c1369a7600fba94d24ba380618f9f75ee0c375c277"}, - {file = "pyzstd-0.17.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da65158c877eac78dcc108861d607c02fb3703195c3a177f2687e0bcdfd519d0"}, - {file = "pyzstd-0.17.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:226ca0430e2357abae1ade802585231a2959b010ec9865600e416652121ba80b"}, - {file = "pyzstd-0.17.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e3a19e8521c145a0e2cd87ca464bf83604000c5454f7e0746092834fd7de84d1"}, - {file = "pyzstd-0.17.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:56ed2de4717844ffdebb5c312ec7e7b8eb2b69eb72883bbfe472ba2c872419e6"}, - {file = "pyzstd-0.17.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc61c47ca631241081c0c99895a1feb56dab4beab37cac7d1f9f18aff06962eb"}, - {file = "pyzstd-0.17.0-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd61757a4020590dad6c20fdbf37c054ed9f349591a0d308c3c03c0303ce221"}, - {file = "pyzstd-0.17.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d6cce91a8ac8ae1aab06684a8bf0dee088405de7f451e1e89776ddc1f40074"}, - {file = "pyzstd-0.17.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc668b67a13bf6213d0a9c09edc1f4842ed680b92fc3c9361f55a904903bfd1f"}, - {file = "pyzstd-0.17.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a67d7ef18715875b31127eb90075c03ced722fd87902b34bca4b807a2ce1e4d9"}, - {file = "pyzstd-0.17.0.tar.gz", hash = "sha256:d84271f8baa66c419204c1dd115a4dec8b266f8a2921da21b81764fa208c1db6"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.13\""} - [[package]] name = "requests" -version = "2.33.0" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["docs", "tutorials"] files = [ - {file = "requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"}, - {file = "requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] @@ -1806,46 +2115,57 @@ urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -test = ["PySocks (>=1.5.6,!=1.5.7)", "pytest (>=3)", "pytest-cov", "pytest-httpbin (==2.1.0)", "pytest-mock", "pytest-xdist"] use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] -name = "roman-numerals-py" -version = "3.1.0" +name = "roman-numerals" +version = "4.1.0" description = "Manipulate well-formed Roman numerals" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["docs"] markers = "python_version >= \"3.11\"" files = [ - {file = "roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c"}, - {file = "roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d"}, + {file = "roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7"}, + {file = "roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2"}, ] -[package.extras] -lint = ["mypy (==1.15.0)", "pyright (==1.1.394)", "ruff (==0.9.7)"] -test = ["pytest (>=8)"] +[[package]] +name = "roman-numerals-py" +version = "4.1.0" +description = "This package is deprecated, switch to roman-numerals." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780"}, + {file = "roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9"}, +] + +[package.dependencies] +roman-numerals = "4.1.0" [[package]] name = "setuptools" -version = "80.9.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" +version = "82.0.1" +description = "Most extensible Python build backend with support for C/C++ extension modules" optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, + {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"}, + {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] [[package]] name = "six" @@ -1861,14 +2181,14 @@ files = [ [[package]] name = "snowballstemmer" -version = "3.0.1" -description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." +version = "3.1.1" +description = "This package provides 36 stemmers for 34 languages generated from Snowball algorithms." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" +python-versions = ">=3.3" groups = ["docs"] files = [ - {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, - {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, + {file = "snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752"}, + {file = "snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260"}, ] [[package]] @@ -2059,70 +2379,85 @@ files = [ [[package]] name = "tomli" -version = "2.2.1" +version = "2.4.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["connect", "dev", "docs"] markers = "python_version == \"3.10\"" files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, ] [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["connect", "dev", "tutorials"] +groups = ["connect", "dev"] files = [ - {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, - {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {tutorials = "python_version < \"3.13\""} [[package]] name = "tzdata" -version = "2025.2" +version = "2026.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["connect", "dev"] +markers = "sys_platform == \"win32\" or sys_platform == \"emscripten\" or python_version == \"3.10\"" files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, + {file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"}, + {file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"}, ] [[package]] @@ -2149,7 +2484,7 @@ version = "0.25.0" description = "Zstandard bindings for Python" optional = false python-versions = ">=3.9" -groups = ["connect"] +groups = ["connect", "dev"] files = [ {file = "zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd"}, {file = "zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7"}, @@ -2258,4 +2593,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "49aa8ff03d24b1dccffacd503a93c37bb9a5c4cf7c74917b14166b1beb8f8a7b" +content-hash = "4d7e71341e48a0ad28bed31d1705deb058a652a0eb7004781ef97e1be489e33d" diff --git a/python/pyproject.toml b/python/pyproject.toml index 05c6e03e5..bee38c4c7 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -72,7 +72,7 @@ pytest = "^9.0.3" pyspark-client = ">=4.0,<4.2" [tool.poetry.group.tutorials.dependencies] -py7zr = "^0.22.0" +py7zr = ">=1.0,<2.0" requests = "^2.33.0" click = "^8.1.8"