diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index eb01f93f..5ad4058a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -10,6 +10,7 @@ on: env: Mindee__ApiKey: ${{ secrets.MINDEE_API_KEY_SE_TESTS }} + Workflow__ID: ${{ secrets.WORKFLOW_ID_SE_TESTS }} jobs: test-nix: diff --git a/docs/code_samples/workflow_execution.txt b/docs/code_samples/workflow_execution.txt new file mode 100644 index 00000000..3f88535e --- /dev/null +++ b/docs/code_samples/workflow_execution.txt @@ -0,0 +1,25 @@ +using Mindee; +using Mindee.Input; + +string apiKey = "my-api-key"; +string filePath = "/path/to/the/file.ext"; +string workflowId = "workflow-id"; + +// Construct a new client +MindeeClient mindeeClient = new MindeeClient(apiKey); + +// Load an input source as a path string +// Other input types can be used, as mentioned in the docs +var inputSource = new LocalInputSource(filePath); + +// Send the document to a workflow execution +var response = await mindeeClient.ExecuteWorkflowAsync(workflowId, inputSource); + +// Alternatively, give it an alias: +// var response = await mindeeClient.ExecuteWorkflowAsync(workflowId, inputSource, new WorkflowOptions(alias: "my-alias")); + +// Print the execution ID to make sure it worked. +System.Console.WriteLine(response.Execution.Id); + +// Print the inference result. +// System.Console.WriteLine(response.Execution.Inference); diff --git a/src/Mindee/Http/GenericParameter.cs b/src/Mindee/Http/GenericParameter.cs new file mode 100644 index 00000000..9e96fe5d --- /dev/null +++ b/src/Mindee/Http/GenericParameter.cs @@ -0,0 +1,52 @@ +using Mindee.Exceptions; +using Mindee.Input; + +namespace Mindee.Http +{ + /// + /// G + /// + public class GenericParameter + { + + /// + /// A local input source. + /// + public LocalInputSource LocalSource { get; } + + /// + /// A URL input source. + /// + public UrlInputSource UrlSource { get; } + + /// + /// Whether to include the full text data for async APIs. + /// This performs a full OCR operation on the server and will increase response time and payload size. + /// + /// It is not available on all API. + public bool FullText { get; } + + /// + /// + /// + /// + /// + /// + /// + public GenericParameter(LocalInputSource localSource, UrlInputSource urlSource, bool fullText) + { + + if (localSource != null && urlSource != null) + { + throw new MindeeException("localSource and urlSource may not both be specified."); + } + if (localSource == null && urlSource == null) + { + throw new MindeeException("One of localSource or urlSource must be specified."); + } + LocalSource = localSource; + UrlSource = urlSource; + FullText = fullText; + } + } +} diff --git a/src/Mindee/Http/IHttpApi.cs b/src/Mindee/Http/IHttpApi.cs index 5aed1df9..78e2e461 100644 --- a/src/Mindee/Http/IHttpApi.cs +++ b/src/Mindee/Http/IHttpApi.cs @@ -43,5 +43,16 @@ Task> DocumentQueueGetAsync( string jobId , CustomEndpoint endpoint = null) where TModel : class, new(); + + /// + /// Send a document to a workflow. + /// + /// The ID of the workflow. + /// + /// Document type. + Task> PostWorkflowExecution( + string workflowId, + WorkflowParameter workflowParameter) + where TModel : class, new(); } } diff --git a/src/Mindee/Http/MindeeApi.cs b/src/Mindee/Http/MindeeApi.cs index 4e938800..2f1f5417 100644 --- a/src/Mindee/Http/MindeeApi.cs +++ b/src/Mindee/Http/MindeeApi.cs @@ -115,6 +115,58 @@ string jobId return handledResponse; } + public async Task> PostWorkflowExecution( + string workflowId, + WorkflowParameter workflowParameter) + where TModel : class, new() + { + var request = new RestRequest( + $"v1/workflows/{workflowId}/executions" + , Method.Post); + + AddWorkflowRequestParameters(workflowParameter, request); + + _logger?.LogInformation($"HTTP POST to {_baseUrl + request.Resource} ..."); + + var response = await _httpClient.ExecutePostAsync(request); + return ResponseHandler>(response); + } + + private static void AddWorkflowRequestParameters(WorkflowParameter workflowParameter, RestRequest request) + { + if (workflowParameter.LocalSource != null) + { + request.AddFile( + "document", + workflowParameter.LocalSource.FileBytes, + workflowParameter.LocalSource.Filename); + } + else if (workflowParameter.UrlSource != null) + { + request.AddParameter( + "document", + workflowParameter.UrlSource.FileUrl.ToString()); + } + if (workflowParameter.FullText) + { + request.AddQueryParameter(name: "full_text_ocr", value: "true"); + } + + if (workflowParameter.Alias != null) + { + request.AddParameter(name: "alias", value: workflowParameter.Alias); + } + + if (workflowParameter.Priority != null) + { + request.AddParameter( + name: "priority", + value: workflowParameter.Priority != null ? + workflowParameter.Priority.ToString()?.ToLower() : null); + } + + } + private static void AddPredictRequestParameters(PredictParameter predictParameter, RestRequest request) { if (predictParameter.LocalSource != null) diff --git a/src/Mindee/Http/PredictParameter.cs b/src/Mindee/Http/PredictParameter.cs index 12a74c93..3963cdce 100644 --- a/src/Mindee/Http/PredictParameter.cs +++ b/src/Mindee/Http/PredictParameter.cs @@ -1,4 +1,3 @@ -using Mindee.Exceptions; using Mindee.Input; namespace Mindee.Http @@ -6,31 +5,14 @@ namespace Mindee.Http /// /// Parameter required to use the predict feature. /// - public sealed class PredictParameter + public sealed class PredictParameter : GenericParameter { - /// - /// A local input source. - /// - public LocalInputSource LocalSource { get; } - - /// - /// A URL input source. - /// - public UrlInputSource UrlSource { get; } - /// /// Want an OCR result ? /// /// It is not available on all API. public bool AllWords { get; } - /// - /// Whether to include the full text data for async APIs. - /// This performs a full OCR operation on the server and will increase response time and payload size. - /// - /// It is not available on all API. - public bool FullText { get; } - /// /// Want the cropping result about the document? /// @@ -38,32 +20,21 @@ public sealed class PredictParameter public bool Cropper { get; } /// - /// + /// Prediction parameters for requests. /// - /// - /// - /// - /// - /// + /// Local input source containing the file. + /// Source URL to use. + /// Whether to include the full OCR response in the payload (compatible APIs only). + /// Whether to include the full text in the payload (compatible APIs only) + /// Whether to crop the document before enqueuing on the API. public PredictParameter( LocalInputSource localSource, UrlInputSource urlSource, bool allWords, bool fullText, - bool cropper) + bool cropper) : base(localSource, urlSource, fullText) { - if (localSource != null && urlSource != null) - { - throw new MindeeException("localSource and urlSource may not both be specified."); - } - if (localSource == null && urlSource == null) - { - throw new MindeeException("One of localSource or urlSource must be specified."); - } - LocalSource = localSource; - UrlSource = urlSource; AllWords = allWords; - FullText = fullText; Cropper = cropper; } } diff --git a/src/Mindee/Http/WorkflowParameter.cs b/src/Mindee/Http/WorkflowParameter.cs new file mode 100644 index 00000000..5668efdb --- /dev/null +++ b/src/Mindee/Http/WorkflowParameter.cs @@ -0,0 +1,40 @@ +using System; +using Mindee.Input; + +namespace Mindee.Http +{ + /// + /// Parameter required to use the workflow feature. + /// + public class WorkflowParameter : GenericParameter + { + /// + /// Alias to give to the file. + /// + public string Alias { get; } + + + /// + /// Priority to give to the execution. + /// + public ExecutionPriority? Priority { get; } + + /// + /// Workflow parameters. + /// + /// Local input source containing the file. + /// Source URL to use. + /// Whether to include the full text in the payload (compatible APIs only) + /// Alias to give to the document. + /// Priority to give to the document. + public WorkflowParameter( + LocalInputSource localSource, + UrlInputSource urlSource, bool fullText, + string alias, ExecutionPriority? priority) : base(localSource, urlSource, + fullText) + { + Alias = alias; + Priority = priority; + } + } +} diff --git a/src/Mindee/MindeeClient.cs b/src/Mindee/MindeeClient.cs index 7bc57813..3011eb52 100644 --- a/src/Mindee/MindeeClient.cs +++ b/src/Mindee/MindeeClient.cs @@ -609,6 +609,71 @@ LocalInputSource inputSource throw new MindeeException($"Could not complete after {retryCount} attempts."); } + /// + /// Send a local file to a workflow execution. + /// + /// The workflow id. + /// + /// + /// + /// + public async Task> ExecuteWorkflowAsync( + string workflowId, + LocalInputSource inputSource, + WorkflowOptions workflowOptions = null, + PageOptions pageOptions = null) + { + _logger?.LogInformation("Sending '{}' to workflow '{}'...", inputSource.Filename, workflowId); + + if (pageOptions != null && inputSource.IsPdf()) + { + inputSource.FileBytes = _pdfOperation.Split( + new SplitQuery(inputSource.FileBytes, pageOptions)).File; + } + + workflowOptions ??= new WorkflowOptions(); + + return await _mindeeApi.PostWorkflowExecution( + workflowId, + new WorkflowParameter( + localSource: inputSource, + urlSource: null, + alias: workflowOptions.Alias, + priority: workflowOptions.Priority, + fullText: workflowOptions.FullText + )); + } + + /// + /// Send a remote file to a workflow execution. + /// + /// The workflow id. + /// + /// + /// + public async Task> ExecuteWorkflowAsync( + string workflowId, + UrlInputSource inputSource, + WorkflowOptions workflowOptions = null) + { + _logger?.LogInformation("Asynchronous parsing of {} ...", inputSource.FileUrl); + + if (workflowOptions == null) + { + workflowOptions = new WorkflowOptions(); + } + + return await _mindeeApi.PostWorkflowExecution( + workflowId, + new WorkflowParameter( + localSource: null, + urlSource: inputSource, + alias: workflowOptions.Alias, + priority: workflowOptions.Priority, + fullText: workflowOptions.FullText + )); + } + /// /// Load a local prediction. /// Typically used when wanting to load from a webhook callback. diff --git a/src/Mindee/MindeeClientOptions.cs b/src/Mindee/MindeeClientOptions.cs index bbd4eafc..3379054d 100644 --- a/src/Mindee/MindeeClientOptions.cs +++ b/src/Mindee/MindeeClientOptions.cs @@ -1,5 +1,6 @@ using System; using Mindee.Exceptions; +using Mindee.Input; namespace Mindee { @@ -102,4 +103,42 @@ public AsyncPollingOptions(double initialDelaySec = 2.0, double intervalSec = 1. IntervalMilliSec = (int)Math.Floor(IntervalSec * 1000); } } + + /// + /// Options for workflow executions. + /// + public sealed class WorkflowOptions + { + + /// + /// Alias to give to the file. + /// + public string Alias { get; } + + + /// + /// Priority to give to the execution. + /// + public ExecutionPriority? Priority { get; } + + + /// + /// Whether to include the full text data for async APIs. + /// This performs a full OCR operation on the server and will increase response time and payload size. + /// + public bool FullText { get; } + + /// + /// Options for workflow executions. + /// + /// + /// + /// + public WorkflowOptions(string alias = null, ExecutionPriority? priority = null, bool fullText = false) + { + Alias = alias; + Priority = priority; + FullText = fullText; + } + } } diff --git a/src/Mindee/Parsing/Common/Execution.cs b/src/Mindee/Parsing/Common/Execution.cs new file mode 100644 index 00000000..f634ed5f --- /dev/null +++ b/src/Mindee/Parsing/Common/Execution.cs @@ -0,0 +1,96 @@ +using System; +using System.Text.Json.Serialization; +using Mindee.Input; +using Mindee.Product.Generated; + +namespace Mindee.Parsing.Common +{ + /// + /// Representation of a workflow execution. + /// + public class Execution where TInferenceModel : class, new() + { + /// + /// Identifier for the batch to which the execution belongs. + /// + [JsonPropertyName("batch_name")] + public string BatchName { get; set; } + + /// + /// The time at which the execution started. + /// + [JsonPropertyName("created_at")] + [JsonConverter(typeof(DateTimeJsonConverter))] + public DateTime? CreatedAt { get; set; } + + /// + /// File representation within a workflow execution. + /// + [JsonPropertyName("file")] + public ExecutionFile File { get; set; } + + /// + /// Identifier for the execution. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Deserialized inference object. + /// + [JsonPropertyName("inference")] + public TInferenceModel Inference { get; set; } + + /// + /// Priority of the execution. + /// + [JsonPropertyName("priority")] + [JsonConverter(typeof(StringEnumConverter))] + public ExecutionPriority Priority { get; set; } + + /// + /// The time at which the file was tagged as reviewed. + /// + [JsonPropertyName("reviewed_at")] + [JsonConverter(typeof(DateTimeJsonConverter))] + public DateTime? ReviewedAt { get; set; } + + /// + /// The time at which the file was uploaded to a workflow. + /// + [JsonPropertyName("available_at")] + [JsonConverter(typeof(DateTimeJsonConverter))] + public DateTime? AvailableAt { get; set; } + + /// + /// Reviewed fields and values. + /// + [JsonPropertyName("reviewed_prediction")] + public GeneratedV1Document ReviewedPrediction { get; set; } + + /// + /// Execution Status. + /// + [JsonPropertyName("status")] + public string Status { get; set; } + + /// + /// Execution type. + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// The time at which the file was uploaded to a workflow. + /// + [JsonPropertyName("uploaded_at")] + [JsonConverter(typeof(DateTimeJsonConverter))] + public DateTime? UploadedAt { get; set; } + + /// + /// Identifier for the workflow. + /// + [JsonPropertyName("workflow_id")] + public string WorkflowId { get; set; } + } +} diff --git a/src/Mindee/Parsing/Common/ExecutionFile.cs b/src/Mindee/Parsing/Common/ExecutionFile.cs new file mode 100644 index 00000000..61b4442a --- /dev/null +++ b/src/Mindee/Parsing/Common/ExecutionFile.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace Mindee.Parsing.Common +{ + /// + /// File name. + /// + public class ExecutionFile + { + /// + /// File name. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Optional alias for the file. + /// + [JsonPropertyName("alias")] + public string Alias { get; set; } + } +} diff --git a/src/Mindee/Parsing/Common/ExecutionPriority.cs b/src/Mindee/Parsing/Common/ExecutionPriority.cs new file mode 100644 index 00000000..78e749d9 --- /dev/null +++ b/src/Mindee/Parsing/Common/ExecutionPriority.cs @@ -0,0 +1,59 @@ +using System; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Mindee.Input +{ + /// + /// Priority for a workflow execution. + /// + public enum ExecutionPriority + { + /// + /// Low priority. + /// + [EnumMember(Value = "low")] Low, + + /// + /// Medium priority. + /// + [EnumMember(Value = "medium")] Medium, + + /// + /// Hight priority. + /// + [EnumMember(Value = "high")] High + } + + /// + /// Deserializer for the ExecutionPriority enum. + /// + /// + public class StringEnumConverter : JsonConverter where T : struct, Enum + { + /// + /// Read a JSON value. + /// + /// + /// + /// + /// + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string value = reader.GetString(); + return Enum.TryParse(value, true, out var result) ? result : default; + } + + /// + /// Retrieves a JSON value. + /// + /// + /// + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString().ToLower()); + } + } +} diff --git a/src/Mindee/Parsing/Common/WorkflowResponse.cs b/src/Mindee/Parsing/Common/WorkflowResponse.cs new file mode 100644 index 00000000..79da4375 --- /dev/null +++ b/src/Mindee/Parsing/Common/WorkflowResponse.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; +using Mindee.Product.Generated; + +namespace Mindee.Parsing.Common +{ + /// + /// Represents the server response after a document is sent to a workflow. + /// + public class WorkflowResponse : CommonResponse where TModel : class, new() + { + /// + /// Set the prediction model used to parse the document. + /// + [JsonPropertyName(("execution"))] + public Execution Execution { get; set; } + + + /// + /// Default product is GeneratedV1. + /// + public class Default : WorkflowResponse + { + } + } +} diff --git a/tests/Mindee.IntegrationTests/Workflow/WorkflowTest.cs b/tests/Mindee.IntegrationTests/Workflow/WorkflowTest.cs new file mode 100644 index 00000000..3bc776e0 --- /dev/null +++ b/tests/Mindee.IntegrationTests/Workflow/WorkflowTest.cs @@ -0,0 +1,25 @@ +using Mindee.Input; + +namespace Mindee.IntegrationTests.Workflow +{ + [Trait("Category", "Integration tests")] + public class WorkflowTest + { + [Fact] + public async Task Given_AWorkflowIDShouldReturnACorrectWorkflowObject() + { + var apiKey = Environment.GetEnvironmentVariable("Mindee__ApiKey"); + var client = TestingUtilities.GetOrGenerateMindeeClient(apiKey); + var inputSource = new LocalInputSource("Resources/products/financial_document/default_sample.jpg"); + + string currentDateTime = DateTime.Now.ToString("yyyy-MM-dd-HH:mm:ss"); + var alias = "dotnet-" + currentDateTime; + WorkflowOptions options = new WorkflowOptions(alias, ExecutionPriority.Low); + var response = await client.ExecuteWorkflowAsync(Environment.GetEnvironmentVariable("Workflow__ID"), inputSource, options); + + Assert.Equal(ExecutionPriority.Low, response.Execution.Priority); + Assert.Equal(alias, response.Execution.File.Alias); + + } + } +} diff --git a/tests/Mindee.UnitTests/Workflow/WorklowTest.cs b/tests/Mindee.UnitTests/Workflow/WorklowTest.cs new file mode 100644 index 00000000..5e508b6a --- /dev/null +++ b/tests/Mindee.UnitTests/Workflow/WorklowTest.cs @@ -0,0 +1,142 @@ +using System.Text.Json; +using Mindee.Http; +using Mindee.Input; +using Mindee.Parsing.Common; +using Mindee.Pdf; +using Mindee.Product.Generated; +using Moq; + +namespace Mindee.UnitTests.Workflow +{ + [Trait("Category", "Workflow")] + public abstract class WorklowTest + { + private readonly MindeeClient client; + private readonly Mock mockedClient; + private readonly Mock mindeeApi; + + protected WorklowTest() + { + mindeeApi = new Mock(); + Mock pdfOperation = new Mock(); + client = new MindeeClient(pdfOperation.Object, mindeeApi.Object); + mockedClient = new Mock(); + } + + [Fact] + public async Task GivenAWorkflowMockFileShouldReturnAValidWorkflowObject() + { + // Arrange + var file = new FileInfo("src/test/resources/file_types/pdf/blank_1.pdf"); + var workflowResponse = new WorkflowResponse { Execution = new Execution(), ApiRequest = null }; + + mindeeApi.Setup(api => api.PostWorkflowExecution( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(workflowResponse); + + // Act + var execution = await client.ExecuteWorkflowAsync( + "", + new LocalInputSource(file)); + + // Assert + Assert.NotNull(execution); + mindeeApi.Verify(api => api.PostWorkflowExecution( + It.IsAny(), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task SendingADocumentToAnExecutionShouldDeserializeResponseCorrectly() + { + // Arrange + var jsonFile = File.ReadAllText("src/test/resources/workflows/success.json"); + var mockResponse = JsonSerializer.Deserialize>(jsonFile); + + mockedClient.Setup(mindeeClient => mindeeClient.ExecuteWorkflowAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockResponse); + + string workflowId = "07ebf237-ff27-4eee-b6a2-425df4a5cca6"; + string filePath = "src/test/resources/products/financial_document/default_sample.jpg"; + var inputSource = new LocalInputSource(filePath); + + // Act + var response = await mockedClient.Object.ExecuteWorkflowAsync(workflowId, inputSource); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.ApiRequest); + Assert.Null(response.Execution.BatchName); + Assert.Null(response.Execution.CreatedAt); + Assert.Null(response.Execution.File.Alias); + Assert.Equal("default_sample.jpg", response.Execution.File.Name); + Assert.Equal("8c75c035-e083-4e77-ba3b-7c3598bd1d8a", response.Execution.Id); + Assert.Null(response.Execution.Inference); + Assert.Equal(ExecutionPriority.Medium, response.Execution.Priority); + Assert.Null(response.Execution.ReviewedAt); + Assert.Null(response.Execution.ReviewedPrediction); + Assert.Equal("processing", response.Execution.Status); + Assert.Equal("manual", response.Execution.Type); + Assert.Equal("2024-11-13T13:02:31.699190", + response.Execution.UploadedAt?.ToString("yyyy-MM-ddTHH:mm:ss.ffffff")); + Assert.Equal(workflowId, response.Execution.WorkflowId); + + mockedClient.Verify(mindeeClient => mindeeClient.ExecuteWorkflowAsync( + workflowId, + It.Is(source => source.Filename == inputSource.Filename), + It.IsAny(), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task SendingADocumentToAnExecutionWithPriorityAndAliasShouldDeserializeResponseCorrectly() + { + // Arrange + var jsonFile = File.ReadAllText("src/test/resources/workflows/success_low_priority.json"); + var mockResponse = JsonSerializer.Deserialize>(jsonFile); + + mockedClient.Setup(mindeeClient => mindeeClient.ExecuteWorkflowAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockResponse); + + string workflowId = "07ebf237-ff27-4eee-b6a2-425df4a5cca6"; + string filePath = "src/test/resources/products/financial_document/default_sample.jpg"; + var inputSource = new LocalInputSource(filePath); + + // Act + var response = await mockedClient.Object.ExecuteWorkflowAsync(workflowId, inputSource); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.ApiRequest); + Assert.Null(response.Execution.BatchName); + Assert.Null(response.Execution.CreatedAt); + Assert.Equal("low-priority-sample-test", response.Execution.File.Alias); + Assert.Equal("default_sample.jpg", response.Execution.File.Name); + Assert.Equal("b743e123-e18c-4b62-8a07-811a4f72afd3", response.Execution.Id); + Assert.Null(response.Execution.Inference); + Assert.Equal(ExecutionPriority.Low, response.Execution.Priority); + Assert.Null(response.Execution.ReviewedAt); + Assert.Null(response.Execution.ReviewedPrediction); + Assert.Equal("processing", response.Execution.Status); + Assert.Equal("manual", response.Execution.Type); + Assert.Equal("2024-11-13T13:17:01.315179", + response.Execution.UploadedAt?.ToString("yyyy-MM-ddTHH:mm:ss.ffffff")); + Assert.Equal(workflowId, response.Execution.WorkflowId); + + mockedClient.Verify(mindeeClient => mindeeClient.ExecuteWorkflowAsync( + workflowId, + It.Is(source => source.Filename == inputSource.Filename), + It.IsAny(), + It.IsAny()), Times.Once); + } + } +} diff --git a/tests/test_code_samples.sh b/tests/test_code_samples.sh index 89ed6e80..e16249ce 100755 --- a/tests/test_code_samples.sh +++ b/tests/test_code_samples.sh @@ -9,7 +9,7 @@ API_KEY=$3 if [ -z "${ACCOUNT}" ]; then echo "ACCOUNT is required"; exit 1; fi if [ -z "${ENDPOINT}" ]; then echo "ENDPOINT is required"; exit 1; fi -for f in $(find docs/code_samples -maxdepth 1 -name "*.txt" | sort -h) +for f in $(find docs/code_samples -maxdepth 1 -name "*.txt" -not -name "workflow_execution.txt" | sort -h) do echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "${f}"