diff --git a/documentation/how_to/use_diffo_provider_instance_extension.livemd b/documentation/how_to/use_diffo_provider_instance_extension.livemd new file mode 100644 index 0000000..a595786 --- /dev/null +++ b/documentation/how_to/use_diffo_provider_instance_extension.livemd @@ -0,0 +1,588 @@ + + +# Using the Diffo Provider Instance Extension + +```elixir +Mix.install( + [ + {:diffo, "~> 0.1.2"} + ], + consolidate_protocols: false +) +``` + +## Overview + +Diffo is a Telecommunications Management Forum (TMF) Service and Resource Manager, built for autonomous networks. + +It is implemented using the [Ash Framework](https://www.ash-hq.org) leveraging core and community extensions including some created and maintained by [diffo-dev](https://github.com/diffo-dev/). As such it is highly customizable using Spark DSL and as necessary Elixir. +If you are not already familiar with Ash then please explore [Ash Get Started](https://hexdocs.pm/ash/get-started.html) + +First ensure you've explored the Diffo Livebook for an introduction to Diffo: [![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo%2Ddev%2Fdiffo%2Fblob%2Fdev%2Fdiffo.livemd) + +In this 'Diffo Provider Instance Extension' livebook you will learn about: + +* TMF Services and Resources +* Building your own Domain +* Declaring a Composite Resource +* Using the Assigner +* Composing a Resource from partially assigned Resources + +### Installing Neo4j and Configuring Boltx + +Diffo uses the [Ash Neo4j DataLayer](https://github.com/diffo-dev/ash_neo4j), which requires Neo4j to be installed + +While [Neo4j community edition](https://github.com/neo4j/neo4j) is open source and you can build from source it is likely that you'll use an installation. + +[AshNeo4j](https://github.com/diffo-dev/ash_neo4j) uses [neo4j](https://github.com/neo4j/neo4j) which must be installed and running. You can install latest major Neo4j versions from the community tab at [Neo4j Deployment Center](https://neo4j.com/deployment-center/?desktop-gdb), or use the [5.26.8 direct link](https://neo4j.com/download-thanks/?edition=community&release=5.26.8&flavour=rpm) + +When you install neo4j you'll typically have a default username and password. Take note of this and any other non-standard config. + +Update the configuration below as necessary and evaluate. + +```elixir +config = [ + uri: "bolt://localhost:7687", + auth: [username: "neo4j", password: "password"], + user_agent: "diffoLivebook/1", + pool_size: 15, + max_overflow: 3, + prefix: :default, + name: Bolt, + log: false, + log_hex: false +] +``` + +Boltx needs a process in your supervision tree, this will start one with the config if not already running: + +```elixir +AshNeo4j.BoltxHelper.start(config) +``` + +Now you should be able to verify that Neo4j is running: + +```elixir +AshNeo4j.BoltxHelper.is_connected() +``` + +You can get all nodes related to other nodes the following query: + +```elixir +AshNeo4j.Cypher.run("MATCH (n1)-[r]->(n2) RETURN r, n1, n2 LIMIT 50") +``` + +It is helpful to have a Neo4j browser open locally, typically: + +http://localhost:7474/browser/ + +Once you connect and issue a query like the one above you'll be able to explore the results interactively. + + + +**OPTIONAL** If you want to clear your database you can evaluate: + +```elixir +AshNeo4j.Neo4jHelper.delete_all() +``` + +## TMF Services and Resources + +TMF Services are network services with industry standard structure and API that are operated for you by a Provider Entity. Ideally TMF Services are as abstract as possible, such that the Consumer specifies their intent (often by selecting a service from a catalog and providing minimal configuration of features and/or characteristics) allowing the provider to deliver the service as best it sees fit. This is powerful as it allows the service to perform advanced uses cases, like move, technology change, and allow the provider to optimise and even dynamically recompose the service. +TMF Resources are generally a network resource that needs to be assigned to provide a service. They are generally too low level to have value on their own and where possible are entirely hidden from the product layer. + +TMF Services are generally composed of services and/or resources. TMF Resources can also be composed of resources (but not services). + +TMF Services and Resources are similar in that they each have a Specification, and are defined by Features and Characteristics. They also can have outgoing relationships with other services and resources, indeed this is fundamental to composition and in particular resource assignment. + +Resources are generally created/managed/owned by a Provider, and assigned to a Consumer. Often the assignment is effectively a lease during which period the consumer has exclusive use of the resource under the provider's conditions, effectively 'owning' the resource. + +When a Provider creates a pool of resources this is known as 'allocation'. For instance a VLAN pool may contain VLAN ID's 0..4095, and perhaps a new pool is inherently allocated with either a new interface, or the creation of a logical L2 VLAN domain. + +When a Consumer is leased a resource this is assignment. + +Assigment is effectively a request for a relationship from a Provider Resource 'back up' to a Consumer Service or Resource. There are different variants on this: + +* Specific Resource assignment - the specific resource requested by the Consumer is assigned +* 'To specification' Resource assignment - an entire resource is assigned by the Provider, allocation may be 'just in time' +* Partial Resource assignment - a partial resource is assigned by the Provider, the consumer is aware of the 'pool resource'. +* Specific partial resource assignment - a partial resource requested by the Consumer is assigned + +In all cases the assignment is only successful if the Provider allows the requested relationship to occur from it back to the Consumer. + +Partial resource assignment uses a relationship characteristic to indicate which part of the resource is optionally requested and ultimately assigned. + +```elixir +Diffo.Provider.list_instances!() +``` + +## Building your own Domain + +Diffo is an open source, extensible, declarative, intent driven TMF Service and Resource Manager. + +In the Introductory Livebook we used the Diffo.Provider domain API and created basic TMF Services and Resources. While this is OK for getting to know Diffo, we actually want to build/declare domain specific functionality which means we will build our own [Ash.Domain](https://hexdocs.pm/ash/Ash.Domain.html). + +Diffo.Provider.Instance modelling a Service or Resource actually uses the Diffo.Provider.BaseInstance [Spark.Dsl.Fragment](https://hexdocs.pm/spark/Spark.Dsl.Fragment.html). There is no need to evaluate the Diffo.Provider.Instance below, it is already defined. + +```elixir +defmodule Diffo.Provider.Instance do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Instance - Ash Resource for a TMF Service or Resource Instance + """ + alias Diffo.Provider.BaseInstance + + use Ash.Resource, + fragments: [BaseInstance], + domain: Diffo.Provider + + resource do + description "An Ash Resource for a TMF Service or Resource Instance" + plural_name :instances + end +end +``` + +Diffo also has an inbuilt Spark DSL extension [Diffo.Provider.Instance.Extension](https://hexdocs.pm/diffo/Diffo.Provider.Instance.Extension.html) which provides DSL and functions for use in building domain specific services and resources. + +Currently it has DSL to allow you to declare specification, features and characteristics. It can be used for services or resources. +Feature and Instance Characteristics can have payloads defined by [Ash.TypedStruct](https://hexdocs.pm/ash/Ash.TypedStruct.html). TypedStruct are DSL specified types which are effectively lightweight embedded resources. We've extended both [AshJason](https://hexdocs.pm/ash_jason/) and [AshOutstanding](https://hexdocs.pm/ash_outstanding/) to support Ash.TypedStruct. + +For partial resource allocation and assignment we've created Diffo.Provider.Assigner. It is used by the host resource, which declares a characteristic with an Diffo.Provider.AssignableValue TypedStruct. Allocation is managed within the Provider domain using this characteristic. Assignment to Services or Resources is via 'reverse' type: "assignedTo" relationships enriched by relationship characteristics. + +We can still use the Diffo.Provider API's noting that they will return Diffo.Provider.Instance rather than our specific domain resource, but we'll use our own domain API linked to specific actions. + +Let's imagine a Compute domain which operates GPU and NPU resources. We want to expose a Cluster composite resource which can be dynamically composed of a number of GPU and NPU cores. + +Each instance of Cluster could be created on Consumer demand as a 'container' for the GPI and NPU core partial resources. + +Each of the GPU and NPU Resource instances is created and managed by the Provider and is effectively a resource pool for individualy assignable cores. + +We'll define these Resources in a Compute domain which exposes an API. + +```elixir +defmodule Diffo.Compute do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Compute - example domain + """ + use Ash.Domain, + otp_app: :diffo, + validate_config_inclusion?: false + + alias Diffo.Compute.GPU + #alias Diffo.Compute.NPU + alias Diffo.Compute.Cluster + + resources do + resource GPU do + define :get_gpu_by_id, action: :read, get_by: :id + define :build_gpu, action: :build + define :define_gpu, action: :define + define :relate_gpu, action: :relate + define :assign_gpu_core, action: :assign_core + end + + #resource NPU do + #define :get_npu_by_id, action: :read, get_by: :id + #define :build_npu, action: :build + #define :define_npu, action: :define + #define :relate_npu, action: :relate + #define :assign_npu_core, action: :assign_core + #end + + resource Cluster do + define :get_cluster_by_id, action: :read, get_by: :id + define :build_cluster, action: :build + define :define_cluster, action: :define + define :relate_cluster, action: :relate + end + end +end +``` + +## Declaring a Composite Resource + +We will start by declaring the Cluster Resource. It is going to be a composite resource, where it can be assigned individual GPU and NPU cores via resource relationships. It is an Ash.Resource incorporating the Diffo.Provide.BaseInstance fragment. + +```elixir +defmodule Diffo.Compute.Cluster do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Cluster - Cluster Resource Instance + """ + + alias Diffo.Provider.BaseInstance + alias Diffo.Provider.Instance.Relationship + alias Diffo.Provider.Instance.Characteristic + alias Diffo.Provider.Instance.ActionHelper + alias Diffo.Compute + alias Diffo.Compute.ClusterValue + + use Ash.Resource, + fragments: [BaseInstance], + domain: Compute + + resource do + description "An Ash Resource representing a Cluster" + plural_name :Clusters + end + + specification do + id "4bcfc4c9-e776-4878-a658-e8d81857bed7" + name "cluster" + type :resourceSpecification + description "A Cluster Resource Instance" + category "Network Resource" + end + + characteristics do + characteristic :cluster, ClusterValue + end + + actions do + create :build do + description "creates a new Cluster resource instance for build" + accept [:id, :name, :type, :which] + argument :specified_by, :uuid, public?: false + argument :relationships, {:array, :struct} + argument :features, {:array, :uuid}, public?: false + argument :characteristics, {:array, :uuid}, public?: false + argument :places, {:array, :struct} + argument :parties, {:array, :struct} + + change set_attribute(:type, :resource) + + change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) + + change after_action(fn changeset, result, _context -> + ActionHelper.build_after(changeset, result, Compute, :get_cluster_by_id) + end) + + change load [:href] + upsert? false + end + + update :define do + description "defines the cluster" + argument :characteristic_value_updates, {:array, :term} + + change after_action(fn changeset, result, _context -> + with {:ok, _result} <- Characteristic.update_values(result, changeset), + {:ok, cluster} <- Compute.get_cluster_by_id(result.id), + do: {:ok, cluster} + end) + end + + update :relate do + description "relates the cluster with other instances" + argument :relationships, {:array, :struct} + + change after_action(fn changeset, result, _context -> + with {:ok, _cluster} <- Relationship.relate_instance(result, changeset), + {:ok, cluster} <- Compute.get_cluster_by_id(result.id), + do: {:ok, cluster} + end) + end + end +end +``` + +And of course we'll need a ClusterValue TypedStruct for the Cluster Resource's cluster characteristic: + +```elixir +defmodule Diffo.Compute.ClusterValue do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + ClusterValue - AshTyped Struct for Cluster Characteristic Value + """ + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + jason do + pick [:name, :sections, :length, :loss, :technology] + compact true + end + + outstanding do + expect [:loss] + end + + typed_struct do + field :name, :string, description: "the cluster name" + + field :gpu_cores, :integer, + default: 0, + constraints: [min: 0], + description: "the number of GPU cores in the cluster" + + field :npu_cores, :integer, + default: 0, + constraints: [min: 0], + description: "the number of NPU cores in the cluster" + end + + defimpl String.Chars do + def to_string(struct) do + inspect(struct) + end + end +end +``` + +Now that should be enough to allow us to create a Cluster instance + +```elixir +defmodule Diffo.Compute.Test do + alias Diffo.Provider + alias Diffo.Provider.Instance.Place + alias Diffo.Provider.Instance.Party + + def create_exchange_place do + exchange = + Provider.create_place!(%{ + id: "DONC", + name: :exchangeId, + href: "place/telstra/DONC", + referredType: :GeographicSite + }) + + %Place{id: exchange.id, role: :NetworkSite} + end + + def create_provider_party do + provider = + Provider.create_party!(%{ + id: "Compute", + name: :organizationId, + referredType: :Organization + }) + + %Party{id: provider.id, role: :Provider} + end +end + +places = [Diffo.Compute.Test.create_exchange_place()] +parties = [Diffo.Compute.Test.create_provider_party()] +cluster_1 = Diffo.Compute.build_cluster!(%{name: "cluster_1", places: places, parties: parties}) +``` + +We can render this Ash Resource as json: + +```elixir +Jason.encode!(cluster_1, pretty: true) |> IO.puts +``` + +## Using the Assigner + +We'll now define a GPU Resource which uses the Diffo.Provider.Assigner functionality. + +```elixir +defmodule Diffo.Compute.GPU do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + GPU - GPU Resource Instance + """ + + alias Diffo.Provider.BaseInstance + alias Diffo.Provider.Instance.Relationship + alias Diffo.Provider.Instance.Characteristic + alias Diffo.Provider.Instance.ActionHelper + alias Diffo.Provider.Assigner + alias Diffo.Provider.Assignment + alias Diffo.Provider.AssignableValue + alias Diffo.Compute + alias Diffo.Compute.GPUValue + + use Ash.Resource, + fragments: [BaseInstance], + domain: Compute + + resource do + description "An Ash Resource representing a GPU" + plural_name :gpus + end + + specification do + id "ad50073f-17e0-45cb-b9b1-aa4296876156" + name "gpu" + type :resourceSpecification + description "A GPU Resource Instance" + category "Network Resource" + end + + characteristics do + characteristic :gpu, GPUValue + characteristic :cores, AssignableValue + end + + actions do + create :build do + description "creates a new GPU resource instance for build" + accept [:id, :name, :type, :which] + argument :specified_by, :uuid, public?: false + argument :relationships, {:array, :struct} + argument :features, {:array, :uuid}, public?: false + argument :characteristics, {:array, :uuid}, public?: false + argument :places, {:array, :struct} + argument :parties, {:array, :struct} + + change set_attribute(:type, :resource) + + change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) + + change after_action(fn changeset, result, _context -> + ActionHelper.build_after(changeset, result, Compute, :get_gpu_by_id) + end) + + change load [:href] + upsert? false + end + + update :define do + description "defines the GPU" + argument :characteristic_value_updates, {:array, :term} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Compute.get_gpu_by_id(result.id), + do: {:ok, result} + end) + end + + update :relate do + description "relates the GPU with other instances" + argument :relationships, {:array, :struct} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Relationship.relate_instance(result, changeset), + {:ok, result} <- Compute.get_gpu_by_id(result.id), + do: {:ok, result} + end) + end + + update :assign_core do + description "relates the GPU with an instance by assigning a core" + argument :assignment, :struct, constraints: [instance_of: Assignment] + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Assigner.assign(result, changeset, :cores, :core), + {:ok, result} <- Compute.get_gpu_by_id(result.id), + do: {:ok, result} + end) + end + end +end +``` + +And we must define the GPUValue TypedStruct, used in the GPU's gpu characteristic: + +```elixir +defmodule Diffo.Compute.GPUValue do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + CardValue - AshTyped Struct for GPU Characteristic Value + """ + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + jason do + pick [:name, :family, :model, :technology] + compact true + end + + outstanding do + expect [:name] + end + + typed_struct do + field :name, :string, description: "the GPU name" + + field :family, :atom, description: "the GPU family name" + + field :model, :string, description: "the GPU model name" + + field :technology, :atom, description: "the GPU technology" + end + + defimpl String.Chars do + def to_string(struct) do + inspect(struct) + end + end +end +``` + +Now we'll create a couple of GPU instances: + +```elixir +alias Diffo.Compute + +gpu_1 = Compute.build_gpu!(%{name: "GPU 1"}) +gpu_2 = Compute.build_gpu!(%{name: "GPU 2"}) +``` + +We need to define each GPU instance, in this case defining the gpu Characteristic AssignableValue performs the allocation - in this case setting how many GPU cores are available. + +```elixir +updates = [ + gpu: [family: :nvidia, model: "GeForce RTX5090", technology: :blackwell], + cores: [first: 1, last: 680, free: 680, type: "tensor"] +] + +gpu_1 = Compute.define_gpu!(gpu_1, %{characteristic_value_updates: updates}) +gpu_2 = Compute.define_gpu!(gpu_2, %{characteristic_value_updates: updates}) +``` + +The GPU's core characteristic is an AssignableValue, now we've allocated it we can use it to keep track of how many cores are free (unassigned). We can render one as json: + +```elixir +Jason.encode!(gpu_1, pretty: true) |> IO.puts +``` + +## Composing a Resource from partially assigned Resources + +Now we can auto-assign GPU cores from each GPU to our cluster_1. We'll assign 3 cores from gpu_1, and one from gpu_2. + +```elixir +alias Diffo.Provider.Assignment +alias Diffo.Compute + +assignment = %{assignment: %Assignment{assignee_id: cluster_1.id, operation: :auto_assign}} +gpu_1 = Compute.assign_gpu_core!(gpu_1, assignment) +gpu_1 = Compute.assign_gpu_core!(gpu_1, assignment) +gpu_1 = Compute.assign_gpu_core!(gpu_1, assignment) +gpu_2 = Compute.assign_gpu_core!(gpu_2, assignment) +``` + +Now our cluster should have a core from each gpu. Check in the neo4j browser for the type: :assignedTo Relationship from the gpu_1 and gpu_2 to the clusters. There should be four, each with a Relationship Characteristic of core, with a value of the assigned core, e.g. 1, 2. + +Also the gpu will show each assignedTo relationship, since these are forward relationships. These should also show the relationship characteristic: + +```elixir +Jason.encode!(gpu_1, pretty: true) |> IO.puts +``` + +Make sure you have a look at it in the neo4j browser. There should be Relationship nodes with a role of :assignedTo from each GPU resource instance to the cluster_1 resource instance. Each Relationship should be defined by a Characteristic with the assigned core number. +There is no central assignment table, rather the relationships ARE the assignments. + +As an exercise, clone the GPU resource to create an NPU resource and assign some NPU cores from it to your cluster. Check that the assigned NPU cores are unique. + +What happens when there are none left to assign? +What happens when I request a specific assignment from an instance to which the partial resource is already assigned? + +### What Next? + +In this tutorial you've used Diffo's Provider Instance Extension to define a Compute domain with a composite Cluster resource which is comprised of assigned GPU cores from GPU resources and NPU cores from NPU resources. + +The composite Cluster Resource is a fully fledged TMF Resource which can itself be related to consuming TMF Service and/or Resources. + +If you find Diffo useful please visit and star on [github](https://github.com/diffo-dev/diffo/). Feel free to join discussions and raise issues to discuss PR's. diff --git a/mix.lock b/mix.lock index fca90e7..72f9357 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "ash": {:hex, :ash, "3.7.6", "a0358e8467da4e2a94855542d07d7fca8e74cb6bc89c42af2181b4caa91f8415", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6003aa4dec5868e6371c3bf2efdb89507c59c05f5dbec13a13b73a92b938a258"}, - "ash_jason": {:hex, :ash_jason, "3.0.1", "3e4102ea78e76bb319514249cf491a9a72789dedb579f82c03cf6af772a44a9a", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:spark, ">= 2.1.21 and < 3.0.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "4a451f8163e3c3e1a49ded99fcbd3405d9880b85e9c1a0d41f7c3e94576b5bce"}, + "ash_jason": {:hex, :ash_jason, "3.0.2", "919ac953f99d3caf56cfc1d30ae4fd5457125f5927c45d75db13a44b38b9fb37", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:spark, ">= 2.1.21 and < 3.0.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "f959b2d1e09df42681311c17c7f078dbaf0695ea2442454fd5e92b906ca871dc"}, "ash_neo4j": {:hex, :ash_neo4j, "0.2.11", "41433b79c8dfe1f371faf411757c03b44cb56f14870375e80b348819fb17ddf1", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:boltx, ">= 0.0.6", [hex: :boltx, repo: "hexpm", optional: false]}], "hexpm", "b26a165908af327d45e951794c0fc1eaf4002d23239c41d6c4c01b1203471293"}, "ash_outstanding": {:hex, :ash_outstanding, "0.2.3", "dc8ec13028ea7bd1d74b46569b9db08f0d275d63700e2418d9e33fe4b21af2eb", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:outstanding, "~> 0.2.4", [hex: :outstanding, repo: "hexpm", optional: false]}], "hexpm", "05e2718b59937d9f7e77b7bc90f70e8f28c3f328de7cabf3ea55ca04a1abed52"}, "ash_state_machine": {:hex, :ash_state_machine, "0.2.12", "c0f7ebb8a176584f70c6ed196b7d0118c930d73e0590ade705d2dddc48aa7311", [:mix], [{:ash, ">= 3.4.66 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}], "hexpm", "394ce761ce82358e3c715e1cae6c5cf1390be27c03a8b661f2e5a2fda849873d"}, @@ -33,7 +33,7 @@ "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, "rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"}, "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, - "spark": {:hex, :spark, "2.3.5", "f30d30ecc3b4ab9b932d9aada66af7677fc1f297a2c349b0bcec3eafb9f996e8", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "0e9d339704d5d148f77f2b2fef3bcfc873a9e9bb4224fcf289c545d65827202f"}, + "spark": {:hex, :spark, "2.3.7", "04018d1bc47613a40d4a804395883d5cc5cfdb05cd14282de35da99e1bb14ee3", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "c6799d4385c4556f540652203359c090941cc3b11e783cd985aa0168914f310d"}, "spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"}, "splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"}, "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},