Package: datapackage-pipelines
Datapackage PipelinesThe BasicsWhat is it?datapackage-pipelinesis a framework for declarative stream-processing of tabular data. It is built upon the concepts and tooling of the Frictionless Data project.PipelinesThe basic concept in this framework is the pipeline.A pipeline has a list of processing steps, and it generates a singledata packageas its output. Each step is executed in aprocessorand consists of the following stages:Modify the data package descriptor- For example: add metadata, add or remove resources, change resources' data schema etc.Process resources- Each row of each resource is processed sequentially. The processor can drop rows, add new ones or modify their contents.Return stats- If necessary, the processor can report a dictionary of data which will be returned to the user when the pipeline execution terminates. This can be used, for example, for calculating quality measures for the processed data.Not every processor needs to do all of these. In fact, you would often find each processing step doing only one of these.pipeline-spec.yamlfilePipelines are defined in a declarative way, and not in code. One or more pipelines can be defined in apipeline-spec.yamlfile. This file specifies the list of processors (referenced by name) and the execution parameters for each of the processors.Here's an example of apipeline-spec.yamlfile:worldbank-co2-emissions:title:CO2 emission data from the World Bankdescription:Data per year, provided in metric tons per capita.environment:DEBUG:truepipeline:-run:update_packageparameters:name:'co2-emissions'title:'CO2emissions(metrictonspercapita)'homepage:'http://worldbank.org/'-run:loadparameters:from:"http://api.worldbank.org/v2/en/indicator/EN.ATM.CO2E.PC?downloadformat=excel"name:'global-data'format:xlsheaders:4-run:set_typesparameters:resources:global-datatypes:"[12][0-9]{3}":type:number-run:dump_to_zipparameters:out-file:co2-emissions-wb.zipIn this example we see one pipeline calledworldbank-co2-emissions. Its pipeline consists of 4 steps:update_package: This is a library processor  (see below), which modifies the data-package's descriptor (in our case: the initial, empty descriptor) - addingname,titleand other properties to the datapackage.load: This is another library processor, which loads data into the data-package.
This resource has anameand afromproperty, pointing to the remote location of the data.set_types: This processor assigns data types to fields in the data. In this example, field headers looking like years will be assigned thenumbertype.dump_to_zip: Create a zipped and validated datapackage with the provided file name.Also, we have provided some metadata:title: Title of a pipelinedescription: Description of a pipelineenvironment: Dictionary of environment variables to be set for all the pipeline's steps. For examples, it can be used to change the behaviour of the underlayingrequestslibrary -https://requests.readthedocs.io/en/master/user/advanced/#ssl-cert-verificationFull JSONSchema of thepipeline-spec.yamlfile can be foundhereMechanicsAn important aspect of how the pipelines are run is the fact that data is passed in streams from one processor to another. If we get "technical" here, then each processor is run in its own dedicated process, where the datapackage is read from itsstdinand output to itsstdout. The important thing to note here is that no processor holds the entire data set at any point.This limitation is by design - to keep the memory and disk requirements of each processor limited and independent of the dataset size.Quick StartFirst off, create apipeline-spec.yamlfile in your current directory. You can take the above file if you just want to try it out.Then, you can either installdatapackage-pipelineslocally - note thatPython 3.6or higher is required due to use ofType Hintingand advancedasynciouse:$pipinstalldatapackage-pipelinesYou should now be able to use thedppcommand:$dpp
AvailablePipelines:
-./worldbank-co2-emissions(*)$$dpprun--verbose./worldbank-co2-emissions
RUNNING./worldbank-co2-emissions
Collectingdependencies
Runningasynctask
Waitingforcompletion
Asynctaskstarting
Searchingforexistingcaches
Buildingprocesschain:
-update_package
-load
-set_types
-dump_to_zip
-(sink)DONE/Users/adam/code/dhq/specstore/dpp_repo/datapackage_pipelines/specs/../lib/update_package.py
load:DEBUG:StartingnewHTTPconnection(1):api.worldbank.org:80
load:DEBUG:http://api.worldbank.org:80"GET /v2/en/indicator/EN.ATM.CO2E.PC?downloadformat=excel HTTP/1.1"200308736load:DEBUG:http://api.worldbank.org:80"GET /v2/en/indicator/EN.ATM.CO2E.PC?downloadformat=excel HTTP/1.1"200308736load:DEBUG:StartingnewHTTPconnection(1):api.worldbank.org:80
load:DEBUG:http://api.worldbank.org:80"GET /v2/en/indicator/EN.ATM.CO2E.PC?downloadformat=excel HTTP/1.1"200308736load:DEBUG:http://api.worldbank.org:80"GET /v2/en/indicator/EN.ATM.CO2E.PC?downloadformat=excel HTTP/1.1"200308736set_types:INFO:(<dataflows.processors.set_type.set_typeobjectat0x10a5c79b0>,)load:INFO:Processed264rows
set_types:INFO:Processed264rows
DONE/Users/adam/code/dhq/specstore/dpp_repo/datapackage_pipelines/specs/../lib/load.py
DONE/Users/adam/code/dhq/specstore/dpp_repo/datapackage_pipelines/specs/../lib/set_types.py
dump_to_zip:INFO:Processed264rows
DONE/Users/adam/code/dhq/specstore/dpp_repo/datapackage_pipelines/manager/../lib/internal/sink.py
DONE/Users/adam/code/dhq/specstore/dpp_repo/datapackage_pipelines/specs/../lib/dump_to_zip.py
DONEV./worldbank-co2-emissions{'bytes':692741,'count_of_rows':264,'dataset_name':'co2-emissions','hash':'4dd18effcdfbf5fc267221b4ffc28fa4'}INFO:RESULTS:
INFO:SUCCESS:./worldbank-co2-emissions{'bytes':692741,'count_of_rows':264,'dataset_name':'co2-emissions','hash':'4dd18effcdfbf5fc267221b4ffc28fa4'}Alternatively, you could use ourDockerimage:$dockerrun-it-v`pwd`:/pipelines:rw\frictionlessdata/datapackage-pipelines
<available-pipelines>

$dockerrun-it-v`pwd`:/pipelines:rw\frictionlessdata/datapackage-pipelinesrun./worldbank-co2-emissions
<execution-logs>The Command Line Interface -dppRunning a pipeline from the command line is done using thedpptool.Runningdppwithout any argument, will show the list of available pipelines. This is done by scanning the current directory and its subdirectories, searching forpipeline-spec.yamlfiles and extracting the list of pipeline specifications described within.Each pipeline has an identifier, composed of the path to thepipeline-spec.yamlfile and the name of the pipeline, as defined within that description file.In order to run a pipeline, you usedpp run <pipeline-id>.You can also usedpp run allfor running all pipelines anddpp run dirtyto run the just thedirtypipelines (more on that later on).Deeper look into pipelinesProcessor ResolutionAs previously seen, processors are referenced by name.This name is, in fact, the name of a Python script containing the processing code (minus the.pyextension). When trying to find where is the actual code that needs to be executed, the processor resolver will search in these predefined locations:First of all, it will try to find a custom processor with that name in the directory of thepipeline-spec.yamlfile.
Processor names support the dot notation, so you could writemycode.custom_processorand it will try to find a processor namedcustom_processor.pyin themycodedirectory, in the same path as the pipeline spec file.
For this specific resolving phase, if you would write..custom_processorit will try to find that processor in the parent directory of the pipeline spec file.
(read on for instructions on how to write custom processors)In case the processor name looks likemyplugin.somename, it will try to find a processor namedsomenamein themypluginplugin. That is - it will see if there's an installed plugin which is calledmyplugin, and if so, whether that plugin publishes a processor calledsomename(more on plugins below).If no processor was found until this point, it will try to search for this processor in the processor search path. The processor search path is taken from the environment variableDPP_PROCESSOR_PATH. Each of the:separated paths in the path is considered as a possible starting point for resolving the processor.Finally, it will try to find that processor in the Standard Processor Library which is bundled with this package.Excluding directories form scanning for pipeline specsBy default.*directories are excluded from scanning, you can add additional directory patterns for
exclusion by creating a.dpp_spec_ignorefile at the project root. This file has similar syntax
to .gitignore and will exclude directories from scanning based on glob pattern matching.For example, the following file will ignoretest*directories including inside subdirectories
and/docsdirectory will only be ignored at the project root directorytest*
/docsCachingBy setting thecachedproperty on a specific pipeline step toTrue, this step's output will be stored on disk (in the.cachedirectory, in the same location as thepipeline-spec.yamlfile).Rerunning the pipeline will make use of that cache, thus avoiding the execution of the cached step and its precursors.Internally, a hash is calculated for each step in the pipeline - which is based on the processor's code, it parameters and the hash of its predecessor. If a cache file exists with exactly the same hash as a specific step, then we can remove it (and its predecessors) and use that cache file as an input to the pipelineThis way, the cache becomes invalid in case the code or execution parameters changed (either for the cached processor or in any of the preceding processors).Dirty tasks and keeping stateThe cache hash is also used for seeing if a pipeline is "dirty". When a pipeline completes executing successfully,dppstores the cache hash along with the pipeline id. If the stored hash is different than the currently calculated hash, it means that either the code or the execution parameters were modified, and that the pipeline needs to be re-run.dppworks with two storage backends. For running locally, it uses a pythonsqlite DBto store the current state of each running task, including the last result and cache hash. The state DB file is stored in a file named.dpp.dbin the same directory thatdppis being run from.For other installations, especially ones using the task scheduler, it is recommended to work with theRedisbackend. In order to enable the Redis connection, simply set theDPP_REDIS_HOSTenvironment variable to point to a running Redis instance.Pipeline DependenciesYou can declare that a pipeline is dependent on another pipeline or datapackage. This dependency is considered when calculating the cache hashes of a pipeline, which in turn affect the validity of cache files and the "dirty" state:For pipeline dependencies, the hash of that pipeline is used in the calculationFor datapackage dependencies, thehashproperty in the datapackage is used in the calculationIf the dependency is missing, then the pipeline is marked as 'unable to be executed'.Declaring dependencies is done by adependenciesproperty to a pipeline definition in thepipeline-spec.yamlfile.
This property should contain a list of dependencies, each one is an object with the following formats:A single key namedpipelinewhose value is the pipeline id to depend onA single key nameddatapackagewhose value is the identifier (or URL) for the datapackage to depend onExample:cat-vs-dog-populations:dependencies:-pipeline:./geo/region-areal-datapackage:http://pets.net/data/dogs-per-region/datapackage.json-datapackage:http://pets.net/data/dogs-per-region...ValidatingEach processor's input is automatically validated for correctness:The datapackage is always validated before being passed to a processor, so there's no possibility for a processor to modify a datapackage in a way that renders it invalid.Data is not validated against its respective JSON Table Schema, unless explicitly requested by setting thevalidateflag to True in the step's info.
This is done for two main reasons:Performance wise, validating the data in every step is very CPU intensiveIn some cases you modify the schema in one step and the data in another, so you would only like to validate the data once all the changes were madeIn any case, when using theset_typesstandard processor, it will validate and transform the input data with the new types..Dataflows integrationDataflowsis the successor of datapackage-pipelines and provides a more
Pythonic interface to running pipelines. You can integrate dataflows within pipeline specs using theflowattribute
instead ofrun. For example, given the following flow file, saved undermy-flow.py:from dataflows import Flow, dump_to_path, load, update_package

def flow(parameters, datapackage, resources, stats):
    stats['multiplied_fields'] = 0

    def multiply(field, n):
        def step(row):
            row[field] = row[field] * n
            stats['multiplied_fields'] += 1
        return step

    return Flow(update_package(name='my-datapackage'),
                multiply('my-field', 2))And apipeline-spec.yamlin the same directory:my-flow:
  pipeline:
  - run: load_resource
    parameters:
      url: http://example.com/my-datapackage/datapackage.json
      resource: my-resource
  - flow: my-flow
  - run: dump_to_pathYou can run the pipeline usingdpp run my-flow.The Standard Processor LibraryA few built in processors are provided with the library.update_packageAdds meta-data to the data-package.Parameters:Any allowed property (according to thespec) can be provided here.Example:-run:update_packageparameters:name:routes-to-mordorlicense:CC-BY-SA-4author:Frodo Baggins <frodo@shire.me>contributors:-samwise gamgee <samwise1992@yahoo.com>update_resourceAdds meta-data to the resource.Parameters:resourcesA name of a resource to operate onA regular expression matching resource namesA list of resource namesThe index of the resource in the packageif omitted indicates operation should be done on all resourcesmetadata- A dictionary containing any allowed property (according to thespec).Example:-run:update_resourceparameters:resources:['resource1']metadata:path:'new-path.csv'loadLoads data into the package, infers the schema and optionally casts values.Parameters:from- location of the data that is to be loaded. This can be either:a local path (e.g. /path/to/the/data.csv)a remote URL (e.g.https://path.to/the/data.csv)Other supported links, based on the current support of schemes and formats intabulatora local path or remote URL to a datapackage.json file (e.g.https://path.to/data_package/datapackage.json)a reference to an environment variable containing the source location, in the form ofenv://ENV_VARa tuple containing (datapackage_descriptor, resources_iterator)resources- optional, relevant only if source points to a datapackage.json file or datapackage/resource tuple. Value should be one of the following:Name of a single resource to loadA regular expression matching resource names to loadA list of resource names to load'None' indicates to load all resourcesThe index of the resource in the packagevalidate- Should data be casted to the inferred data-types or not. Relevant only when not loading data from datapackage.other options - based on the loaded file, extra options (e.g. sheet for Excel files etc., see the link to tabulator above)printerJust prints whatever it sees. Good for debugging.Parameters:num_rows- modify the number of rows to preview, printer will print multiple samples of this number of rows from different places in the streamlast_rows- how many of the last rows in the stream to print. optional, defaults to the value of num_rowsfields- optional, list of field names to previewresources- optional, allows to limit the printed resources, same semantics as load processor resources argumentset_typesSets data types and type options to fields in streamed resources, and make sure that the data still validates with the new types.This allows to make modifications to the existing table schema, and usually to the default schema fromstream_remote_resources.Parameters:resources- Which resources to modify. Can be:List of strings, interpreted as resource names to streamString, interpreted as a regular expression to be used to match resource namesIf omitted, all resources in datapackage are streamed.regex- if set toFalsefield names will be interpreted as strings not as regular expressions (Trueby default)types- A map between field names and field definitions.field nameis either simply the name of a field, or a regular expression matching multiple fields.field definitionis an object adhering to theJSON Table Schema spec. You can usenullinstead of an object to remove a field from the schema.Example:-run:add_resourcesparameters:name:example-resourceurl:http://example.com/my-csv-file.csvencoding:"iso-8859-2"-run:stream_remote_resources-run:set_typesparameters:resources:example-resourcetypes:age:type:integer"yearly_score_[0-9]{4}":type:number"dateofbirth":type:dateformat:"%d/%m/%Y""socialsecuritynumber":nullload_metadataLoads metadata from an existing data-package.Parameters:Loads the metadata from the data package located aturl.All properties of the loaded datapackage will be copied (except theresources)Example:-run:load_metadataparameters:url:http://example.com/my-datapackage/datapackage.jsonload_resourceLoads a tabular resource from an existing data-package.Parameters:Loads the resource specified in theresourceparameter from the data package located aturl.
All properties of the loaded resource will be copied -pathandschemaincluded.url- a URL pointing to the datapackage in which the required resource residesresource- can beList of strings, interpreted as resource names to loadString, interpreted as a regular expression to be used to match resource namesan integer, indicating the index of the resource in the data package (0-based)limit-rows- if provided, will limit the number of rows fetched from the source. Takes an integer value which specifies how many rows of the source to stream.log-progress-rows- if provided, will log the loading progress. Takes an integer value which specifies the number of rows interval at which to log the progress.stream- if provided and is set to false, then the resource will be added to the datapackage but not streamed.resources- can be used instead ofresourceproperty to support loading resources and modify the output resource metadataValue is a dict containing mapping between source resource name to load and dict containing descriptor updates to apply to the loaded resourcerequired- if provided and set to false, will not fail if datapackage is not available or resource is missingExample:-run:load_resourceparameters:url:http://example.com/my-datapackage/datapackage.jsonresource:my-resource-run:load_resourceparameters:url:http://example.com/my-other-datapackage/datapackage.jsonresource:1-run:load_resourceparameters:url:http://example.com/my-datapackage/datapackage.jsonresources:my-resource:name:my-renamed-resourcepath:my-renamed-resource.csvconcatenateConcatenates a number of streamed resources and converts them to a single resource.Parameters:sources- Which resources to concatenate. Same semantics asresourcesinstream_remote_resources.If omitted, all resources in datapackage are concatenated.Resources to concatenate must appear in consecutive order within the data-package.target- Target resource to hold the concatenated data. Should define at least the following properties:name- name of the resourcepath- path in the data-package for this file.If omitted, the target resource will receive the nameconcatand will be saved atdata/concat.csvin the datapackage.fields- Mapping of fields between the sources and the target, so that the keys are thetargetfield names, and values are lists ofsourcefield names.This mapping is used to create the target resources schema.Note that the target field name isalwaysassumed to be mapped to itself.Example:-run:concatenateparameters:target:name:multi-year-reportpath:data/multi-year-report.csvsources:'report-year-20[0-9]{2}'fields:activity:[]amount:['2009_amount','Amount','AMOUNT[USD]','$$$']In this example we concatenate all resources that look likereport-year-<year>, and output them to themulti-year-reportresource.The output contains two fields:activity, which is calledactivityin all sourcesamount, which has varying names in different resources (e.g.Amount,2009_amount,amountetc.)joinJoins two streamed resources."Joining" in our case means taking thetargetresource, and adding fields to each of its rows by looking up data in thesourceresource.A special case for the join operation is when there is no target stream, and all unique rows from the source are used to create it.
This mode is calleddeduplicationmode - The target resource will be created and  deduplicated rows from the source will be added to it.Parameters:source- information regarding thesourceresourcename- name of the resourcekey- One ofList of field names which should be used as the lookup keyString, which would be interpreted as a Python format string used to form the key (e.g.{<field_name_1>}:{field_name_2})delete- delete from data-package after joining (Falseby default)target- Target resource to hold the joined data. Should define at least the following properties:name- as insourcekey- as insource, ornullfor creating the target resource and performingdeduplication.fields- mapping of fields from the source resource to the target resource.
Keys should be field names in the target resource.
Values can define two attributes:name- field name in the source (by default is the same as the target field name)aggregate- aggregation strategy (how to handle multiplesourcerows with the same key). Can take the following options:sum- summarise aggregated values.
For numeric values it's the arithmetic sum, for strings the concatenation of strings and for other types will error.avg- calculate the average of aggregated values.For numeric values it's the arithmetic average and for other types will err.max- calculate the maximum of aggregated values.For numeric values it's the arithmetic maximum, for strings the dictionary maximum and for other types will error.min- calculate the minimum of aggregated values.For numeric values it's the arithmetic minimum, for strings the dictionary minimum and for other types will error.first- take the first value encounteredlast- take the last value encounteredcount- count the number of occurrences of a specific key
For this method, specifyingnameis not required. In case it is specified,countwill count the number of non-null values for that source field.counters- count the number of occurrences of distinct values
Will return an array of 2-tuples of the form[value, count-of-value].set- collect all distinct values of the aggregated field, unorderedarray- collect all values of the aggregated field, in order of appearanceany- pick any value.By default,aggregatetakes theanyvalue.If neithernameoraggregateneed to be specified, the mapping can map to the empty object{}or tonull.full- Boolean,IfTrue(the default), failed lookups in the source will result in "null" values at the source.ifFalse, failed lookups in the source will result in dropping the row from the target.Important: the "source" resourcemustappear before the "target" resource in the data-package.Examples:-run:joinparameters:source:name:world_populationkey:["country_code"]delete:yestarget:name:country_gdp_2015key:["CC"]fields:population:name:"census_2015"full:trueThe above example aims to create a package containing the GDP and Population of each country in the world.We have one resource (world_population) with data that looks like:country_codecountry_namecensus_2000census_2015UKUnited Kingdom5885700464715810...And another resource (country_gdp_2015) with data that looks like:CCGDP (£m)Net Debt (£m)UK18323181606600...Thejoincommand will match rows in both datasets based on thecountry_code/CCfields, and then copying the value in thecensus_2015field into a newpopulationfield.The resulting data package will have theworld_populationresource removed and thecountry_gdp_2015resource looking like:CCGDP (£m)Net Debt (£m)populationUK1832318160660064715810...A more complex example:-run:joinparameters:source:name:screen_actor_salarieskey:"{production}({year})"target:name:mgm_movieskey:"{title}"fields:num_actors:aggregate:'count'average_salary:name:salaryaggregate:'avg'total_salaries:name:salaryaggregate:'sum'full:falseThis example aims to analyse salaries for screen actors in the MGM studios.Once more, we have one resource (screen_actor_salaries) with data that looks like:yearproductionactorsalary2016Vertigo 2Mr. T150000002016Vertigo 2Robert Downey Jr.70000002015The Fall - ResurrectionJeniffer Lawrence180000002015Alf - The Return to MelmackThe Rock12000000...And another resource (mgm_movies) with data that looks like:titledirectorproducerVertigo 2 (2016)Lindsay LohanLee Ka ShingiRobot - The Movie (2018)Mr. TMr. T...Thejoincommand will match rows in both datasets based on the movie name and production year. Notice how we overcome incompatible fields by using different key patterns.The resulting dataset could look like:titledirectorproducernum_actorsaverage_salarytotal_salariesVertigo 2 (2016)Lindsay LohanLee Ka Shing21100000022000000...filterFilter streamed resources.filteraccepts equality and inequality conditions and tests each row in the selected resources. If none of the conditions validate, the row will be discarded.Parameters:resources- Which resources to apply the filter on. Same semantics asresourcesinstream_remote_resources.in- Mapping of keys to values which translate torow[key] == valueconditionsout- Mapping of keys to values which translate torow[key] != valueconditionsBothinandoutshould be a list of objects. However,outshould only ever have one element.Examples:Filtering just American and European countries, leaving out countries whose main language is English:-run:filterparameters:resources:world_populationin:-continent:america-continent:europe-run:filterparameters:resources:world_populationout:-language:englishTo filteroutby multiple values, you need multiple filter processors, not multipleoutelements. Otherwise some condition will always validate and no rows will be discareded:- run: filter
  parameters:
    resources: world_population
    out:
      - language: english
- run: filter
  parameters:
    resources: world_population
    out:
      - language: swedishsortSort streamed resources by key.sortaccepts a list of resources and a key (as a Python format string on row fields).
It will output the rows for each resource, sorted according to the key (in ascending order by default).Parameters:resources- Which resources to sort. Same semantics asresourcesinstream_remote_resources.sort-by- String, which would be interpreted as a Python format string used to form the key (e.g.{<field_name_1>}:{field_name_2})reverse- Optional boolean, if set to true - sorts in reverse orderExamples:Filtering just American and European countries, leaving out countries whose main language is English:-run:sortparameters:resources:world_populationsort-by:"{country_name}"deduplicateDeduplicates rows in resources based on the resources' primary keydeduplicateaccepts a resource specifier - for each resource, it will output only unique rows (based on the values in the primary key fields). Rows with duplicate primary keys will be ignored.Parameters:resources- Which resources to sort. Same semantics asresourcesinstream_remote_resources.Examples:Deduplicating rows in theworld-populationresource.-run:deduplicateparameters:resources:world_populationduplicateDuplicate a resource.duplicateaccepts the name of a single resource in the datapackage.
It will then duplicate it in the output datapackage, with a different name and path.
The duplicated resource will appear immediately after its original.Parameters:source- Which resources to duplicate. The name of the resource.target-name- Name of the new, duplicated resource.target-path- Path for the new, duplicated resource.duplicate_to_end- Make the duplicate resource appear at the end, rather than immediately after its original.Examples:Filtering just American and European countries, leaving out countries whose main language is English:-run:duplicateparameters:source:original-resourcetarget-name:copy-of-resourcetarget-path:data/duplicate.csvdelete_fieldsDelete fields (columns) from streamed resourcesdelete_fieldsaccepts a list of resources and list of fields to removeNote: if multiple resources provided, all of them should contain all fields to deleteParameters:resources- Which resources to delete columns from. Same semantics asresourcesinstream_remote_resources.fields- List of field (column) names to be removed (exact names or regular expressions for matching field names)regex- if set toFalsefield names will be interpreted as strings not as regular expressions (Trueby default)Examples:Deletingcountry_nameandcensus_2000columns fromworld_populationresource:-run:delete_fieldsparameters:resources:world_populationfields:-country_name-census_2000add_computed_fieldAdd field(s) to streamed resourcesadd_computed_fieldaccepts a list of resources and fields to add to existing resource. It will output the rows for each resource with new field(s) (columns) in it.add_computed_fieldallows to perform various operations before inserting value into targeted field.Parameters:resources- Resources to add field. Same semantics asresourcesinstream_remote_resources.fields- List of operations to be performed on the targeted fields.operation: operation to perform on values of pre-defined columns of the same row. available operation:constant- add a constant valuesum- summed value for given columns in a row.avg- average value from given columns in a row.min- minimum value among given columns in a row.max- maximum value among given columns in a row.multiply- product of given columns in a row.join- joins two or more column values in a row.format- Python format string used to form the value Eg:my name is {first_name}.target- name of the new field.source- list of columns the operations should be performed on (Not required in case offormatandconstant).with- String passed toconstant,formatorjoinoperationsinconstant- used as constant valueinformat- used as Python format string with existing column values Eg:{first_name} {last_name}injoin- used as delimiterExamples:Following example adds 4 new field tosalariesresourcerun:add_computed_fieldparameters:resources:salariesfields:-operation:sumtarget:totalsource:-jan-feb-may-operation:avgtarget:averagesource:-jan-feb-may-operation:formattarget:full_namewith:'{first_name}{last_name}'-operation:constanttarget:statuswith:singleWe have one resource (salaries) with data that looks like:first_namelast_namejanfebmarJohnDoe100200300...The resulting dataset could look like:first_namelast_namelast_namejanfebmaraveragetotalstatusJohnDoeJohn Doe100200300200600single...find_replacefind and replace string or pattern from field(s) valuesParameters:resources- Resources to clean the field values. Same semantics asresourcesinstream_remote_resourcesfields- list of fields to replace valuesname- name of the field to replace valuepatterns- list of patterns to find and replace from fieldfind- String, interpreted as a regular expression to match field valuereplace- String, interpreted as a regular expression to replace matched patternExamples:Following example replaces field values using regular expression and exact string patternsrun:find_replaceparameters:resources:datesfields:-name:yearpatterns:-find:([0-9]{4})( \(\w+\))replace:\1-name:quarterpatterns:-find:Q1replace:'03-31'-find:Q2replace:'06-31'-find:Q3replace:'09-30'-find:Q4replace:'12-31'We have one resource (dates) with data that looks like:yearquarter2000 (1)2000-Q1...The resulting dataset could look like:yearquarter20002000-03-31...unpivotUnpivots, transposes tabular data so that there's only one record per row.Parameters:resources- Resources to unpivot. Same semantics asresourcesinstream_remote_resources.extraKeyFields- List of target field definitions, each definition is an object containing at least these properties (unpivoted column values will go here)name- Name of the target fieldtype- Type of the target fieldextraValueField- Target field definition - an object containing at least these properties (unpivoted cell values will go here)name- Name of the target fieldtype- Type of the target fieldunpivot- List of source field definitions, each definition is an object containing at least these propertiesname- Either simply the name, or a regular expression matching the name of original field to unpivot.keys- A Map between target field name and values for original fieldKeys should be target field names fromextraKeyFieldsValues may be either simply the constant value to insert, or a regular expression matching thename.Examples:Following example will unpivot data into 3 new fields:year,directionandamountparameters:resources:balanceextraKeyFields:-name:yeartype:integer-name:directiontype:stringconstraints:enum:-In-OutextraValueField:name:amounttype:numberunpivot:-name:2015 incomeskeys:year:2015direction:In-name:2015 expenseskeys:year:2015direction:Out-name:2016 incomeskeys:year:2016direction:In-name:2016 expenseskeys:year:2016direction:OutWe have one resource (balance) with data that looks like:company2015 incomes2015 expenses2016 incomes2016 expensesInc100090020001700Org200080030002000...The resulting dataset could look like:companyyeardirectionamountInc2015In1000Inc2015Out900Inc2016In2000Inc2016Out1700Org2015In2000Org2015Out800Org2016In3000Org2016Out2000...Similar result can be accomplished by defining regular expressions instead of constant valuesparameters:resources:balanceextraKeyFields:-name:yeartype:integer-name:directiontype:stringconstraints:enum:-In-OutextraValueField:name:amounttype:numberunpivot:-name:([0-9]{4}) (\\w+)# regex for original columnkeys:year:\\1# First member of group from abovedirection:\\2# Second member of group from abovedump_to_sqlSaves the datapackage to an SQL database.Parameters:engine- Connection string for connecting to the SQL Database (URL syntax)
Also supportsenv://<environment-variable>, which indicates that the connection string should be fetched from the indicated environment variable.
If not specified, assumes a default ofenv://DPP_DB_ENGINEtables- Mapping between resources and DB tables. Keys are table names, values are objects with the following attributes:resource-name- name of the resource that should be dumped to the tablemode- How data should be written to the DB.
Possible values:rewrite(the default) - rewrite the table, all previous data (if any) will be deleted.append- write new rows without changing already existing data.update- update the table based on a set of "update keys".
For each new row, see if there already an existing row in the DB which can be updated (that is, an existing row
with the same values in all of the update keys).
If so - update the rest of the columns in the existing row. Otherwise - insert a new row to the DB.update_keys- Only applicable for theupdatemode. A list of field names that should be used to check for row existence.
If left unspecified, will use the schema'sprimaryKeyas default.indexes- TBDupdated_column- Optional name of a column that will be added to the spewed data with boolean valuetrue- row was updatedfalse- row was insertedupdated_id_column- Optional name of a column that will be added to the spewed data and contain the id of the updated row in DB.dump_to_pathSaves the datapackage to a filesystem path.Parameters:out-path- Name of the output path wheredatapackage.jsonwill be stored.This path will be created if it doesn't exist, as well as internal data-package paths.If omitted, then.(the current directory) will be assumed.force-format- Specifies whether to force all output files to be generated with the same formatifTrue(the default), all resources will use the same formatifFalse, format will be deduced from the file extension. Resources with unknown extensions will be discarded.format- Specifies the type of output files to be generated (ifforce-formatis true):csv(the default) orjsonadd-filehash-to-path: Specifies whether to include file md5 hash into the resource path. Defaults toFalse. IfTrueEmbeds hash in path like so:If original path ispath/to/the/file.extModified path will bepath/to/the/HASH/file.extcounters- Specifies whether to count rows, bytes or md5 hash of the data and where it should be stored. An object with the following properties:datapackage-rowcount: Where should a total row count of the datapackage be stored (default:count_of_rows)datapackage-bytes: Where should a total byte count of the datapackage be stored (default:bytes)datapackage-hash: Where should an md5 hash of the datapackage be stored (default:hash)resource-rowcount: Where should a total row count of each resource be stored (default:count_of_rows)resource-bytes: Where should a total byte count of each resource be stored (default:bytes)resource-hash: Where should an md5 hash of each resource be stored (default:hash)
Each of these attributes could be set to null in order to prevent the counting.
Each property could be a dot-separated string, for storing the data inside a nested object (e.g.stats.rowcount)pretty-descriptor: Specifies how datapackage descriptor (datapackage.json) file will look like:False(default) - descriptor will be written in one line.True- descriptor will have indents and new lines for each key, so it becomes more human-readable.dump_to_zipSaves the datapackage to a zipped archive.Parameters:out-file- Name of the output file where the zipped data will be storedforce-formatandformat- Same as indump_to_pathadd-filehash-to-path- Same as indump_to_pathcounters- Same as indump_to_pathpretty-descriptor- Same as indump_to_pathDeprecated ProcessorsThese processors will be removed in the next major version.add_metadataAlias forupdate_package, is kept for backward compatibility reasons.add_resourceAdds a new external tabular resource to the data-package.Parameters:You should provide anameandurlattributes, and other optional attributes as defined in thespec.urlindicates where the data for this resource resides. Later on, whenstream_remote_resourcesruns, it will use theurl(which is stored in the resource in thedpp:streamedFromproperty) to read the data rows and push them into the pipeline.Note thaturlalso supportsenv://<environment-variable>, which indicates that the resource url should be fetched from the indicated environment variable.  This is useful in case you are supplying a string with sensitive information (such as an SQL connection string for streaming from a database table).Parameters are basically arguments that are passed to atabulator.Streaminstance (see theAPI).
Other than those, you can pass aconstantsparameter which should be a mapping of headers to string values.
When used in conjunction withstream_remote_resources, these constant values will be added to each generated row
(as well as to the default schema).You may also provide a schema here, or use the default schema generated by thestream_remote_resourcesprocessor.
In casepathis specified, it will be used. If not, thestream_remote_resourcesprocessor will assign apathfor you with acsvextension.Example:-run:add_resourceparameters:url:http://example.com/my-excel-file.xlsxsheet:1headers:2-run:add_resourceparameters:url:http://example.com/my-csv-file.csvencoding:"iso-8859-2"stream_remote_resourcesConverts external resources to streamed resources.External resources are ones that link to a remote data source (url or file path), but are not processed by the pipeline and are kept as-is.Streamed resources are ones that can be processed by the pipeline, and their output is saved as part of the resulting datapackage.In case a resource has no schema, a default one is generated automatically here by creating astringfield from each column in the data source.Parameters:resources- Which resources to stream. Can be:List of strings, interpreted as resource names to streamString, interpreted as a regular expression to be used to match resource namesIf omitted, all resources in datapackage are streamed.ignore-missing- if true, then missing resources won't raise an error but will be treated as 'empty' (i.e. with zero rows).
Resources with empty URLs will be treated the same (i.e. will generate an 'empty' resource).limit-rows- if provided, will limit the number of rows fetched from the source. Takes an integer value which specifies how many rows of the source to stream.Example:-run:stream_remote_resourcesparameters:resources:['2014-data','2015-data']-run:stream_remote_resourcesparameters:resources:'201[67]-data'This processor also supports loading plain-text resources (e.g. html pages) and handling them as tabular data - split into rows with a single "data" column.
To enable this behavior, add the following attribute to the resource:"format": "txt".dump.to_sqlAlias fordump_to_sql, is kept for backward compatibility reasons.dump.to_pathSaves the datapackage to a filesystem path.Parameters:out-path- Name of the output path wheredatapackage.jsonwill be stored.This path will be created if it doesn't exist, as well as internal data-package paths.If omitted, then.(the current directory) will be assumed.force-format- Specifies whether to force all output files to be generated with the same formatifTrue(the default), all resources will use the same formatifFalse, format will be deduced from the file extension. Resources with unknown extensions will be discarded.format- Specifies the type of output files to be generated (ifforce-formatis true):csv(the default) orjsonhandle-non-tabular- Specifies whether non tabular resources (i.e. resources without aschema) should be dumped as well to the resulting datapackage.
(See note below for more details)add-filehash-to-path: Specifies whether to include file md5 hash into the resource path. Defaults toFalse. IfTrueEmbeds hash in path like so:If original path ispath/to/the/file.extModified path will bepath/to/the/HASH/file.extcounters- Specifies whether to count rows, bytes or md5 hash of the data and where it should be stored. An object with the following properties:datapackage-rowcount: Where should a total row count of the datapackage be stored (default:count_of_rows)datapackage-bytes: Where should a total byte count of the datapackage be stored (default:bytes)datapackage-hash: Where should an md5 hash of the datapackage be stored (default:hash)resource-rowcount: Where should a total row count of each resource be stored (default:count_of_rows)resource-bytes: Where should a total byte count of each resource be stored (default:bytes)resource-hash: Where should an md5 hash of each resource be stored (default:hash)
Each of these attributes could be set to null in order to prevent the counting.
Each property could be a dot-separated string, for storing the data inside a nested object (e.g.stats.rowcount)pretty-descriptor: Specifies how datapackage descriptor (datapackage.json) file will look like:False(default) - descriptor will be written in one line.True- descriptor will have indents and new lines for each key, so it becomes more human-readable.file-formatters: Specifies custom file format handlers. An object with mapping of format name to Python module and class name.Allows to override the existingcsvandjsonformat handlers or add support for new formats.Note that such changes may make the resulting datapackage incompatible with the frictionlessdata specs and may cause interoperability problems.Example usage:pipeline-spec.yaml(under thecustom-formatterspipeline),XLSXFormat classdump.to_zipSaves the datapackage to a zipped archive.Parameters:out-file- Name of the output file where the zipped data will be storedforce-formatandformat- Same as indump_to_pathhandle-non-tabular- Same as indump_to_pathadd-filehash-to-path- Same as indump_to_pathcounters- Same as indump_to_pathpretty-descriptor- Same as indump_to_pathfile-formatters- Same as indump_to_pathNotedump.to_pathanddump.to_zipprocessors will handle non-tabular resources as well.
These resources must have both aurlandpathproperties, andmust notcontain aschemaproperty.
In such cases, the file will be downloaded from theurland placed in the providedpath.Custom ProcessorsIt's quite reasonable that for any non-trivial processing task, you might encounter a problem that cannot be solved using the standard library processors.For that you might need to write your own processor - here's how it's done.There are two APIs for writing processors - the high level API and the low level API.High Level Processor APIThe high-level API is quite useful for most processor kinds:fromdatapackage_pipelines.wrapperimportprocessdefmodify_datapackage(datapackage,parameters,stats):# Do something with datapackagereturndatapackagedefprocess_row(row,row_index,resource_descriptor,resource_index,parameters,stats):# Do something with rowreturnrowprocess(modify_datapackage=modify_datapackage,process_row=process_row)The high level API consists of one method,processwhich takes two functions:modify_datapackage- which makes changes (if necessary) to the data-package descriptor, e.g. adds metadata, adds resources, modifies resources' schema etc.Can also be used for initialization code when needed.It has these arguments:datapackageis the current data-package descriptor that needs to be modified.
The modified data-package descriptor needs to be returned.parametersis a dict containing the processor's parameters, as provided in thepipeline-spec.yamlfile.statsis a dict which should be modified in order to collect metrics and measurements in the process (e.g. validation checks, row count etc.)process_row- which modifies a single row in the stream. It receives these arguments:rowis a dictionary containing the row to processrow_indexis the index of the row in the resourceresource_descriptoris the descriptor object of the current resource being processedresource_indexis the index of the resource in the data-packageparametersis a dict containing the processor's parameters, as provided in thepipeline-spec.yamlfile.statsis a dict which should be modified in order to collect metrics and measurements in the process (e.g. validation checks, row count etc.)and yields zero or more processed rows.A few examples# Add license informationfromdatapackage_pipelines.wrapperimportprocessdefmodify_datapackage(datapackage,parameters,stats):datapackage['license']='CC-BY-SA'returndatapackageprocess(modify_datapackage=modify_datapackage)# Add new column with constant value to first resource# Column name and value are taken from the processor's parametersfromdatapackage_pipelines.wrapperimportprocessdefmodify_datapackage(datapackage,parameters,stats):datapackage['resources'][0]['schema']['fields'].append({'name':parameters['column-name'],'type':'string'})returndatapackagedefprocess_row(row,row_index,resource_descriptor,resource_index,parameters,stats):ifresource_index==0:row[parameters['column-name']]=parameters['value']returnrowprocess(modify_datapackage=modify_datapackage,process_row=process_row)# Row counterfromdatapackage_pipelines.wrapperimportprocessdefmodify_datapackage(datapackage,parameters,stats):stats['row-count']=0returndatapackagedefprocess_row(row,row_index,resource_descriptor,resource_index,parameters,stats):stats['row-count']+=1returnrowprocess(modify_datapackage=modify_datapackage,process_row=process_row)Low Level Processor APIIn some cases, the high-level API might be too restricting. In these cases you should consider using the low-level API.fromdatapackage_pipelines.wrapperimportingest,spewif__name__=='__main__':withingest()asctx:# Initialisation code, if needed# Do stuff with datapackage# ...stats={}# and resources:defnew_resource_iterator(resource_iterator_):defresource_processor(resource_):# resource_.spec is the resource descriptorforrowinresource_:# Do something with row# Perhaps collect some stats here as wellyieldrowforresourceinresource_iterator_:yieldresource_processor(resource)spew(ctx.datapackage,new_resource_iterator(ctx.resource_iterator),ctx.stats)The above code snippet shows the structure of most low-level processors.We always start with callingingest()- this method gives us the context, containing the execution parameters, the data-package descriptor (as outputed from the previous step) and an iterator on all streamed resources' rows.We finish the processing by callingspew(), which sends the processed data to the next processor in the pipeline.spewreceives:A modified data-package descriptor;A (possibly new) iterator on the resources;A stats object which will be added to stats from previous steps and returned to the user upon completion of the pipeline, and;Optionally, afinalizerfunction that will be called after it has finished iterating on the resources, but before signalling to other processors that it's finished. You could use it to close any open files, for example.A more in-depth explanationspewwrites the data it receives in the following order:First, thedatapackageparameter is written to the stream.
This means that all modifications to the data-package descriptor must be donebeforespewis called.
One common pitfall is to modify the data-package descriptor inside the resource iterator - try to avoid that, as the descriptor that the next processor will receive will be wrong.Then it starts iterating on the resources. For each resource, it iterates on its rows and writes each row to the stream.
This iteration process eventually causes an iteration on the original resource iterator (the one that's returned fromingest). In turn, this causes the process' input stream to be read. Because of the way buffering in operating systems work, "slow" processors will read their input slowly, causing the ones before them to sleep on IO while their more CPU intensive counterparts finish their processing. "quick" processors will not work aimlessly, but instead will either sleep while waiting for incoming data or while waiting for their output buffer to drain.
What is achieved here is that all rows in the data are processed more or less at the same time, and that no processor works too "far ahead" on rows that might fail in subsequent processing steps.Then the stats are written to the stream. This means that stats can be modified during the iteration, and only the value after the iteration finishes will be used.Finally, thefinalizermethod is called (if we received one).A few examplesWe'll start with the same processors from above, now implemented with the low level API.# Add license informationfromdatapackage_pipelines.wrapperimportingest,spewif__name__=='__main__':withingest()asctx:ctx.datapackage['license']='MIT'spew(ctx.datapackage,ctx.resource_iterator)# Add new column with constant value to first resource# Column name and value are taken from the processor's parametersfromdatapackage_pipelines.wrapperimportingest,spewparameters,datapackage,resource_iterator=ingest()datapackage['resources'][0]['schema']['fields'].append({'name':parameters['column-name'],'type':'string'})defnew_resource_iterator(resource_iterator_):defresource_processor(resource_):forrowinresource_:row[parameters['column-name']]=parameters['value']yieldrowfirst_resource=next(resource_iterator_)yield(resource_processor(first_resource))forresourceinresource_iterator_:yieldresourcespew(datapackage,new_resource_iterator(resource_iterator))# Row counterfromdatapackage_pipelines.wrapperimportingest,spew_,datapackage,resource_iterator=ingest()stats={'row-count':0}defnew_resource_iterator(resource_iterator_):defresource_processor(resource_):forrowinresource_:stats['row-count']+=1yieldrowforresourceinresource_iterator_:yieldresource_processor(resource)spew(datapackage,new_resource_iterator(resource_iterator),stats)This next example shows how to implement a simple web scraper. Although not strictly required, web scrapers are usually the first processor in a pipeline. Therefore, they can ignore the incoming data-package and resource iterator, as there's no previous processor generating data:# Web Scraperimportrequestsfromdatapackage_pipelines.wrapperimportingest,spewfromdatapackage_pipelines.utilities.resourcesimportPROP_STREAMINGparameters,_,_=ingest()host=parameters['ckan-instance']package_list_api='https://{host}/api/3/action/package_list'package_show_api='https://{host}/api/3/action/package_show'defscrape_ckan(host_):all_packages=requests.get(package_list_api.format(host=host_))\.json()\.get('result',[])forpackage_idinall_packages:params=dict(id=package_id)package_info=requests.get(package_show_api.format(host=host_),params=params)\.json()\.get('result')ifresultisnotNone:yielddict(package_id=package_id,author=package_info.get('author'),title=package_info.get('title'),)datapackage={'resources':[{PROP_STREAMING:True,# You must set this property for resources being streamed in the pipeline!'name':'package-list','schema':{'fields':[{'name':'package_id','type':'string'},{'name':'author','type':'string'},{'name':'title','type':'string'},]}}]}spew(datapackage,[scrape_ckan(host)])In this example we can see that the initial datapackage is generated from scratch, and the resource iterator is in fact a scraper, yielding rows as they are received from the CKAN instance API.Plugins and Source DescriptorsWhen writing pipelines in a specific problem domain, one might discover that the processing pipelines that are developed follow a certain pattern. Scraping, or fetching source data tends to be similar to one another. Processing, data cleaning, validation are often the same.In order to ease maintenance and avoid boilerplate, adatapackage-pipelinesplugincan be written.Plugins are Python modules nameddatapackage_pipelines_<plugin-name>. Plugins can provide two facilities:Processor packs - you can pack processors revolving a certain theme or for a specific purpose in a plugin. Any processorfooresiding under thedatapackage_pipelines_<plugin-name>.processorsmodule can be used from within a pipeline as<plugin-name>.foo.Pipeline templates - if the classGeneratorexists in thedatapackage_pipelines_<plugin-name>module, it will be used to generate pipeline based on templates - which we call "source descriptors".Source DescriptorsA source descriptor is a yaml file containing information which is used to create a full pipeline.dppwill look for files named<plugin-name>.source-spec.yaml, and will treat them as input for the pipeline generating code - which should be implemented in a class calledGeneratorin thedatapackage_pipelines_<plugin-name>module.This class should inherit fromGeneratorBaseand should implement two methods:generate_pipeline-
which receives the source description and returns an iterator of tuples of the form(id, details).idmight be a pipeline id, in which case details would be an object containing the pipeline definition.
Ifidis of the form:module:, then the details are treated as a source spec from the specified module. This way a generator might generate other source specs.get_schema- which should return a JSON Schema for validating the source description's structureExampleLet's assume we write adatapackage_pipelines_ckanplugin, used to pull data out ofCKANinstances.Here's how such a hypothetical generator would look like:importosimportjsonfromdatapackage_pipelines.generatorsimport\GeneratorBase,slugify,steps,SCHEDULE_MONTHLYSCHEMA_FILE=os.path.join(os.path.dirname(__file__),'schema.json')classGenerator(GeneratorBase):@classmethoddefget_schema(cls):returnjson.load(open(SCHEMA_FILE))@classmethoddefgenerate_pipeline(cls,source):pipeline_id=dataset_name=slugify(source['name'])host=source['ckan-instance']action=source['data-kind']ifaction=='package-list':schedule=SCHEDULE_MONTHLYpipeline_steps=steps(*[('ckan.scraper',{'ckan-instance':host}),('metadata',{'name':dataset_name}),('dump_to_zip',{'out-file':'ckan-datapackage.zip'})])pipeline_details={'pipeline':pipeline_steps,'schedule':{'crontab':schedule}}yieldpipeline_id,pipeline_detailsIn this case, if we store ackan.source-spec.yamlfile looking like this:ckan-instance:example.comname:example-com-list-of-packagesdata-kind:package-listThen when runningdppwe will see an available pipeline named./example-com-list-of-packagesThis pipeline would internally be composed of 3 steps:ckan.scraper,metadataanddump_to_zip.Validating Source DescriptorsSource descriptors can have any structure that best matches the parameter domain of the output pipelines. However, it must have a consistent structure, backed by a JSON Schema file. In our case, the Schema might look like this:{"$schema":"http://json-schema.org/draft-04/schema#","type":"object","properties":{"name":{"type":"string"},"ckan-instance":{"type":"string"},"data-kind":{"type":"string"}},"required":["name","ckan-instance","data-kind"]}dppwill ensure that source descriptor files conform to that schema before attempting to convert them into pipelines using theGeneratorclass.Providing Processor CodeIn some cases, a generator would prefer to provide the processor code as well (alongside the pipeline definition).
In order to to that, the generator can add acodeattribute to any step containing the processor's code. When executed, this step won't try to resolve the processor as usual but will the provided code instead.Running on a scheduledatapackage-pipelinescomes with a celery integration, allowing for pipelines to be run at specific times via acrontablike syntax.In order to enable that, you simply add aschedulesection to yourpipeline-spec.yamlfile (or return a schedule from the generator class, see above), like so:co2-information-cdiac:pipeline:-...schedule:# minute hour day_of_week day_of_month month_of_yearcrontab:'0****'In this example, this pipeline is set to run every hour, on the hour.To run the celery daemon, usecelery's command line interface to rundatapackage_pipelines.app. Here's one way to do it:$python-mceleryworker-B-Adatapackage_pipelines.appRunning this server will start by executing all "dirty" tasks, and continue by executing tasks based on their schedules.As a shortcut for starting the scheduler and the dashboard (see below), you can use a prebuiltDockerimage:$dockerrun-v`pwd`:/pipelines:rw-p5000:5000\frictionlessdata/datapackage-pipelinesserverAnd then browse tohttp://<docker machine's IP address>:5000/to see the current execution status dashboard.Pipeline Dashboard & Status BadgesWhen installed on a server or running using the task scheduler, it's often very hard to know exactly what's running and what the status is of each pipeline.To make things easier, you can spin up the web dashboard to provide an overview of each pipeline's status, its basic info and the result of it latest execution.To start the web server rundpp servefrom the command line and browse tohttp://localhost:5000The environment variableDPP_BASE_PATHwill determine whether dashboard will be served from root or from another base path (example value:/pipelines/).The dashboard endpoints can be made to require authentication by adding a username and password with the environment variablesDPP_BASIC_AUTH_USERNAMEandDPP_BASIC_AUTH_PASSWORD.Even simpler pipeline status is available with a status badge, both for individual pipelines, and for pipeline collections. For a single pipeline, add the full pipeline id to the badge endpoint:http://localhost:5000/badge/path_to/pipelines/my-pipeline-idOr for a collection of pipelines:http://localhost:5000/badge/collection/path_to/pipelines/Note that these badge endpoints will always be exposed regardless ofDPP_BASIC_AUTH_PASSWORDandDPP_BASIC_AUTH_USERNAMEsettings.Integrating with other servicesDatapackage-pipelines can call a predefined webhook on any pipeline event. This might allow for potential integrations with other applications.In order to add a webhook in a specific pipeline, add ahooksproperty in the pipeline definition, which should be a list of URLs.
Whenever that pipeline is queued, starts running or finishes running, all the urls will be POSTed with this payload:{"pipeline":"<pipeline-id>","event":"queue/start/progress/finish","success":true/false(whenapplicable),"errors":[list-of-errors,whenapplicable]}Known Issuesloading a resource which has a lot of data in a single cell raises an exception (#112)