• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

astronomer / astro-cli / bd0008aa-1a67-44ce-bf76-235040fa846e

05 Feb 2026 08:38PM UTC coverage: 33.306% (+0.2%) from 33.15%
bd0008aa-1a67-44ce-bf76-235040fa846e

push

circleci

jeremybeard
Add `astro api` command for authenticated Airflow and Cloud API requests

Introduce the `astro api` top-level command with two subcommands:

  astro api airflow — make requests to the Airflow REST API (local or
  deployed), with automatic version detection (2.x, 3.0.x, 3.0.3+) and
  OpenAPI spec resolution.

  astro api cloud — make requests to the Astro Cloud platform API using
  the current context's bearer token.

Both subcommands support:
  - Endpoint discovery via `ls` and `describe` (parsed from OpenAPI specs)
  - Calling endpoints by operation ID or raw path
  - Pagination (per-page streaming or --slurp into a single array)
  - Response caching with TTL and automatic stale-entry cleanup
  - jq filters and Go-template output formatting
  - Colored JSON output
  - --curl flag to emit equivalent curl commands
  - Magic field syntax for request bodies (@file, :=json, =string)
  - Custom headers and path-parameter overrides

Supporting packages:
  - pkg/openapi: OpenAPI spec fetching, caching, endpoint indexing,
    Airflow version-range mapping, and schema introspection.

Includes unit tests for request handling, output formatting, field
parsing, OpenAPI version mapping, endpoint indexing, and caching.

903 of 2396 new or added lines in 13 files covered. (37.69%)

9 existing lines in 1 file now uncovered.

21761 of 65337 relevant lines covered (33.31%)

8.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.56
/cmd/api/fields.go
1
package api
2

3
import (
4
        "fmt"
5
        "io"
6
        "os"
7
        "strconv"
8
        "strings"
9
)
10

11
const (
12
        keyStart     = '['
13
        keyEnd       = ']'
14
        keySeparator = '='
15
)
16

17
// parseFields parses magic fields and raw fields into a map.
18
// Magic fields (-F) support type conversion and file reading.
19
// Raw fields (-f) are always treated as strings.
20
func parseFields(magicFields, rawFields []string) (map[string]interface{}, error) {
14✔
21
        params := make(map[string]interface{})
14✔
22

14✔
23
        // Parse raw fields first (string-only)
14✔
24
        for _, f := range rawFields {
24✔
25
                if err := parseField(params, f, false); err != nil {
11✔
26
                        return params, err
1✔
27
                }
1✔
28
        }
29

30
        // Parse magic fields (with type conversion)
31
        for _, f := range magicFields {
18✔
32
                if err := parseField(params, f, true); err != nil {
5✔
NEW
33
                        return params, err
×
NEW
34
                }
×
35
        }
36

37
        return params, nil
13✔
38
}
39

40
// parseField parses a single field and adds it to the params map.
41
//
42
//nolint:gocognit // Complex parsing logic for nested field syntax
43
func parseField(params map[string]interface{}, f string, isMagic bool) error {
15✔
44
        var valueIndex int
15✔
45
        var keystack []string
15✔
46
        keyStartAt := 0
15✔
47

15✔
48
parseLoop:
15✔
49
        for i, r := range f {
138✔
50
                switch r {
123✔
51
                case keyStart:
6✔
52
                        if keyStartAt == 0 {
11✔
53
                                keystack = append(keystack, f[0:i])
5✔
54
                        }
5✔
55
                        keyStartAt = i + 1
6✔
56
                case keyEnd:
6✔
57
                        keystack = append(keystack, f[keyStartAt:i])
6✔
58
                case keySeparator:
13✔
59
                        if keyStartAt == 0 {
22✔
60
                                keystack = append(keystack, f[0:i])
9✔
61
                        }
9✔
62
                        valueIndex = i + 1
13✔
63
                        break parseLoop
13✔
64
                }
65
        }
66

67
        if len(keystack) == 0 {
16✔
68
                return fmt.Errorf("invalid key: %q", f)
1✔
69
        }
1✔
70

71
        key := f
14✔
72
        var value interface{}
14✔
73
        if valueIndex == 0 {
15✔
74
                if keystack[len(keystack)-1] != "" {
1✔
NEW
75
                        return fmt.Errorf("field %q requires a value separated by an '=' sign", key)
×
NEW
76
                }
×
77
                // Empty array notation: key[]
78
                value = nil
1✔
79
        } else {
13✔
80
                key = f[0 : valueIndex-1]
13✔
81
                value = f[valueIndex:]
13✔
82
        }
13✔
83

84
        if isMagic && value != nil {
19✔
85
                var err error
5✔
86
                value, err = magicFieldValue(value.(string))
5✔
87
                if err != nil {
5✔
NEW
88
                        return fmt.Errorf("error parsing %q value: %w", key, err)
×
NEW
89
                }
×
90
        }
91

92
        destMap := params
14✔
93
        isArray := false
14✔
94
        var subkey string
14✔
95

14✔
96
        for _, k := range keystack {
34✔
97
                if k == "" {
23✔
98
                        isArray = true
3✔
99
                        continue
3✔
100
                }
101
                if subkey != "" {
20✔
102
                        var err error
3✔
103
                        if isArray {
3✔
NEW
104
                                destMap, err = addParamsSlice(destMap, subkey, k)
×
NEW
105
                                isArray = false
×
106
                        } else {
3✔
107
                                destMap, err = addParamsMap(destMap, subkey)
3✔
108
                        }
3✔
109
                        if err != nil {
3✔
NEW
110
                                return err
×
NEW
111
                        }
×
112
                }
113
                subkey = k
17✔
114
        }
115

116
        if isArray {
17✔
117
                if value == nil {
4✔
118
                        destMap[subkey] = []interface{}{}
1✔
119
                } else {
3✔
120
                        if v, exists := destMap[subkey]; exists {
3✔
121
                                if existSlice, ok := v.([]interface{}); ok {
2✔
122
                                        destMap[subkey] = append(existSlice, value)
1✔
123
                                } else {
1✔
NEW
124
                                        return fmt.Errorf("expected array type under %q, got %T", subkey, v)
×
NEW
125
                                }
×
126
                        } else {
1✔
127
                                destMap[subkey] = []interface{}{value}
1✔
128
                        }
1✔
129
                }
130
        } else {
11✔
131
                if _, exists := destMap[subkey]; exists {
11✔
NEW
132
                        return fmt.Errorf("unexpected override existing field under %q", subkey)
×
NEW
133
                }
×
134
                destMap[subkey] = value
11✔
135
        }
136

137
        return nil
14✔
138
}
139

140
// addParamsMap ensures a nested map exists at the given key and returns it.
141
func addParamsMap(m map[string]interface{}, key string) (map[string]interface{}, error) {
3✔
142
        if v, exists := m[key]; exists {
3✔
NEW
143
                if existMap, ok := v.(map[string]interface{}); ok {
×
NEW
144
                        return existMap, nil
×
NEW
145
                }
×
NEW
146
                return nil, fmt.Errorf("expected map type under %q, got %T", key, v)
×
147
        }
148
        newMap := make(map[string]interface{})
3✔
149
        m[key] = newMap
3✔
150
        return newMap, nil
3✔
151
}
152

153
// addParamsSlice handles adding to an array of objects.
NEW
154
func addParamsSlice(m map[string]interface{}, prevkey, newkey string) (map[string]interface{}, error) {
×
NEW
155
        if v, exists := m[prevkey]; exists {
×
NEW
156
                if existSlice, ok := v.([]interface{}); ok {
×
NEW
157
                        if len(existSlice) > 0 {
×
NEW
158
                                lastItem := existSlice[len(existSlice)-1]
×
NEW
159
                                if lastMap, ok := lastItem.(map[string]interface{}); ok {
×
NEW
160
                                        if _, keyExists := lastMap[newkey]; !keyExists {
×
NEW
161
                                                return lastMap, nil
×
NEW
162
                                        }
×
163
                                }
164
                        }
NEW
165
                        newMap := make(map[string]interface{})
×
NEW
166
                        m[prevkey] = append(existSlice, newMap)
×
NEW
167
                        return newMap, nil
×
168
                }
NEW
169
                return nil, fmt.Errorf("expected array type under %q, got %T", prevkey, v)
×
170
        }
NEW
171
        newMap := make(map[string]interface{})
×
NEW
172
        m[prevkey] = []interface{}{newMap}
×
NEW
173
        return newMap, nil
×
174
}
175

176
// magicFieldValue converts a string value to its appropriate type.
177
// Supports: true, false, null, integers, and file reading with @.
178
func magicFieldValue(v string) (interface{}, error) {
14✔
179
        // File reading
14✔
180
        if strings.HasPrefix(v, "@") {
14✔
NEW
181
                return readFileValue(v[1:])
×
NEW
182
        }
×
183

184
        // Integer conversion
185
        if n, err := strconv.Atoi(v); err == nil {
18✔
186
                return n, nil
4✔
187
        }
4✔
188

189
        // Boolean and null conversion
190
        switch v {
10✔
191
        case "true":
2✔
192
                return true, nil
2✔
193
        case "false":
2✔
194
                return false, nil
2✔
195
        case "null":
2✔
196
                return nil, nil
2✔
197
        default:
4✔
198
                return v, nil
4✔
199
        }
200
}
201

202
// readFileValue reads a value from a file or stdin.
NEW
203
func readFileValue(filename string) (string, error) {
×
NEW
204
        var r io.Reader
×
NEW
205
        if filename == "-" {
×
NEW
206
                r = os.Stdin
×
NEW
207
        } else {
×
NEW
208
                f, err := os.Open(filename)
×
NEW
209
                if err != nil {
×
NEW
210
                        return "", fmt.Errorf("opening file %q: %w", filename, err)
×
NEW
211
                }
×
NEW
212
                defer f.Close()
×
NEW
213
                r = f
×
214
        }
215

NEW
216
        b, err := io.ReadAll(r)
×
NEW
217
        if err != nil {
×
NEW
218
                return "", fmt.Errorf("reading file: %w", err)
×
NEW
219
        }
×
220

NEW
221
        return string(b), nil
×
222
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc