Skip to content

Commit 6e70e89

Browse files
committed
feat: Looker SDK generator for the Dart language
Description: A generator for the Dart language. Note, only supports Looker SDK 4.0. Changes: 1. Dart generator. 2. Tests for dart generator. 3. Generated dart methods and models. 4. Runtime librart for dart. 5. Example dart implementation. 6. Unit tests, 7. End to end test.
1 parent 492323a commit 6e70e89

32 files changed

+85942
-1703
lines changed

dart/looker_sdk/.gitignore

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# the dart lib needs to be included in source control
2+
!lib/**
3+
4+
# the following are excluded from source control.
5+
.env*
6+
temp/
7+
.dart_tool/
8+
.packages
9+
build/
10+
# recommendation is NOT to commit for library packages.
11+
pubspec.lock
12+

dart/looker_sdk/README.md

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Looker API for Dart SDK
2+
3+
A dart implementation of the Looker API. Note that only the Looker 4.0 API is generated.
4+
5+
## Usage
6+
7+
See examples and tests.
8+
9+
Create a `.env` file in the same directory as this `README.md`. The format is as follows:
10+
11+
```
12+
URL=looker_instance_api_endpoint
13+
CLIENT_ID=client_id_from_looker_instance
14+
CLIENT_SECRET=client_secret_from_looker_instance
15+
```
16+
17+
## Add to project
18+
19+
Add following to project `pubspec.yaml` dependencies. Replace `{SHA}` with the sha of the version of the SDK you want to use (a more permanent solution may be added in the future).
20+
21+
```
22+
looker_sdk:
23+
git:
24+
url: https://github.com/looker-open-source/sdk-codegen
25+
ref: {SHA}
26+
path: dart/looker_sdk
27+
```
28+
29+
## Developing
30+
31+
Relies on `yarn` and `dart` being installed. This was developed with `dart` version `2.15.1` so the recommendation is to have a version of dart that is at least at that version.
32+
33+
### Generate
34+
35+
Run `yarn sdk Gen` from the `{reporoot}`. Note that the SDK generator needs to be built `yarn build`. If changing the generator run 'yarn watch` in a separate window. This command generates two files:
36+
37+
1. `{reporoot}/dart/looker_sdk/lib/src/sdk/methods.dart`
38+
2. `{reporoot}/dart/looker_sdk/lib/src/sdk/models.dart`
39+
40+
The files are automatically formatted using `dart` tooling. Ensure that the `dart` binary is available on your path.
41+
42+
### Run example
43+
44+
Run `yarn example` from `{reporoot}/dart/looker_sdk`
45+
46+
### Run tests
47+
48+
Run `yarn test:e2e` from `{reporoot}/dart/looker_sdk` to run end to end tests. Note that this test requires that a `.env` file has been created (see above) and that the Looker instance is running.
49+
50+
Run `yarn test:unit` from `{reporoot}/dart/looker_sdk` to run unit tests. These tests do not require a Looker instance to be running.
51+
52+
Run `yarn test` from `{reporoot}/dart/looker_sdk` to run all tests.
53+
54+
### Run format
55+
56+
Run `yarn format` from `{reporoot}/dart/looker_sdk` to format the `dart` files correctly. This should be run if you change any of the run time library `dart` files. The repo CI will run the linter and will fail if the files have not been correctly formatted.
57+
58+
### Run format
59+
60+
Run `yarn format` from `{reporoot}/dart/looker_sdk` to lint the `dart` files.
61+
62+
## TODOs
63+
64+
1. Make enum mappers private to package. They are currently public as some enums are not used by by the models and a warning for unused class is diaplayed by visual code. It could also be a bug in either the generator or the spec generator (why are enums being generated if they are not being used?).
65+
2. Add optional timeout option to methods.
66+
3. Add additional authorization methods to api keys.
67+
4. Revisit auth session. There is some duplication of methods in generated methods.
68+
5. Add base class for models. Move common props to base class. Maybe add some utility methods for primitive types. Should reduce size of models.dart file.
69+
6. More and better generator tests. They are a bit hacky at that moment.
70+
7. Generate dart documentation.
71+
72+
## Notes
73+
74+
1. Region folding: Dart does not currently support region folding. visual studio code has a generic extension that supports region folding for dart. [Install](https://marketplace.visualstudio.com/items?itemName=maptz.regionfolder) if you wish the generated regions to be honored.

dart/looker_sdk/analysis_options.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include: package:pedantic/analysis_options.yaml
2+
3+
analyzer:

dart/looker_sdk/example/main.dart

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import 'dart:io';
2+
import 'dart:typed_data';
3+
import 'package:looker_sdk/looker_sdk.dart';
4+
import 'package:dotenv/dotenv.dart' show load, env;
5+
6+
void main() async {
7+
load();
8+
var sdk = await createSdk();
9+
await runLooks(sdk);
10+
await runDashboardApis(sdk);
11+
await runConnectionApis(sdk);
12+
}
13+
14+
Future<LookerSDK> createSdk() async {
15+
return await Sdk.createSdk({
16+
'base_url': env['URL'],
17+
'verify_ssl': false,
18+
'credentials_callback': credentialsCallback
19+
});
20+
}
21+
22+
Future<void> runLooks(LookerSDK sdk) async {
23+
try {
24+
var looks = await sdk.ok(sdk.all_looks());
25+
if (looks.isNotEmpty) {
26+
looks.forEach((look) => print(look.title));
27+
var look = await sdk.ok(sdk.run_look(looks[looks.length - 1].id, 'png'));
28+
var dir = Directory('./temp');
29+
if (!dir.existsSync()) {
30+
dir.createSync();
31+
}
32+
File('./temp/look.png').writeAsBytesSync(look as Uint8List);
33+
look = await sdk.ok(sdk.run_look(looks[looks.length - 1].id, 'csv'));
34+
File('./temp/look.csv').writeAsStringSync(look as String);
35+
}
36+
} catch (error, stacktrace) {
37+
print(error);
38+
print(stacktrace);
39+
}
40+
}
41+
42+
Future<void> runDashboardApis(LookerSDK sdk) async {
43+
try {
44+
var dashboards = await sdk.ok(sdk.all_dashboards());
45+
dashboards.forEach((dashboard) => print(dashboard.title));
46+
var dashboard = await sdk.ok(sdk.dashboard(dashboards[0].id));
47+
print(dashboard.toJson());
48+
} catch (error, stacktrace) {
49+
print(error);
50+
print(stacktrace);
51+
}
52+
}
53+
54+
Future<void> runConnectionApis(LookerSDK sdk) async {
55+
try {
56+
var connections = await sdk.ok(sdk.all_connections());
57+
connections.forEach((connection) => print(connection.name));
58+
var connection = await sdk
59+
.ok(sdk.connection(connections[0].name, fields: 'name,host,port'));
60+
print(
61+
'name=${connection.name} host=${connection.host} port=${connection.port}');
62+
var newConnection = WriteDBConnection();
63+
SDKResponse resp = await sdk.connection('TestConnection');
64+
if (resp.statusCode == 200) {
65+
print('TestConnection already exists');
66+
} else {
67+
newConnection.name = 'TestConnection';
68+
newConnection.dialectName = 'mysql';
69+
newConnection.host = 'db1.looker.com';
70+
newConnection.port = '3306';
71+
newConnection.username = 'looker_demoX';
72+
newConnection.password = 'look_your_data';
73+
newConnection.database = 'demo_db2';
74+
newConnection.tmpDbName = 'looker_demo_scratch';
75+
connection = await sdk.ok(sdk.create_connection(newConnection));
76+
print('created ${connection.name}');
77+
}
78+
var updateConnection = WriteDBConnection();
79+
updateConnection.username = 'looker_demo';
80+
connection =
81+
await sdk.ok(sdk.update_connection('TestConnection', updateConnection));
82+
print('Connection updated: username=${connection.username}');
83+
var testResults = await sdk.ok(
84+
sdk.test_connection('TestConnection', tests: DelimList(['connect'])));
85+
if (testResults.isEmpty) {
86+
print('No connection tests run');
87+
} else {
88+
testResults.forEach((i) => print('test result: ${i.name}=${i.message}'));
89+
}
90+
var deleteResult = await sdk.ok(sdk.delete_connection('TestConnection'));
91+
print('Delete result $deleteResult');
92+
} catch (error, stacktrace) {
93+
print(error);
94+
print(stacktrace);
95+
}
96+
}
97+
98+
Map credentialsCallback() {
99+
return {'client_id': env['CLIENT_ID'], 'client_secret': env['CLIENT_SECRET']};
100+
}

dart/looker_sdk/lib/looker_sdk.dart

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export 'src/rtl/api_types.dart';
2+
export 'src/rtl/api_methods.dart';
3+
export 'src/rtl/api_settings.dart';
4+
export 'src/rtl/auth_session.dart';
5+
export 'src/rtl/auth_token.dart';
6+
export 'src/rtl/constants.dart';
7+
export 'src/rtl/sdk.dart';
8+
export 'src/rtl/transport.dart';
9+
export 'src/sdk/methods.dart';
10+
export 'src/sdk/models.dart';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import 'dart:convert';
2+
import 'auth_session.dart';
3+
import 'transport.dart';
4+
5+
class APIMethods {
6+
final AuthSession _authSession;
7+
8+
APIMethods(this._authSession);
9+
10+
Future<T> ok<T>(Future<SDKResponse<T>> future) async {
11+
var response = await future;
12+
if (response.ok) {
13+
return response.result;
14+
} else {
15+
throw Exception(
16+
'Invalid SDK response ${response.statusCode}/${response.statusText}/${response.decodedRawResult}');
17+
}
18+
}
19+
20+
Future<SDKResponse<T>> get<T>(
21+
T Function(dynamic responseData, String contentType) responseHandler,
22+
String path,
23+
[dynamic queryParams,
24+
dynamic body]) async {
25+
var headers = await _getHeaders();
26+
return _authSession.transport.request(
27+
responseHandler,
28+
HttpMethod.get,
29+
'${_authSession.apiPath}$path',
30+
queryParams,
31+
body,
32+
headers,
33+
);
34+
}
35+
36+
Future<SDKResponse> head(String path,
37+
[dynamic queryParams, dynamic body]) async {
38+
var headers = await _getHeaders();
39+
dynamic responseHandler(dynamic responseData, String contentType) {
40+
return null;
41+
}
42+
43+
return _authSession.transport.request(responseHandler, HttpMethod.head,
44+
'${_authSession.apiPath}$path', queryParams, body, headers);
45+
}
46+
47+
Future<SDKResponse<T>> delete<T>(
48+
T Function(dynamic responseData, String contentType) responseHandler,
49+
String path,
50+
[dynamic queryParams,
51+
dynamic body,
52+
T responseInstance]) async {
53+
var headers = await _getHeaders();
54+
return _authSession.transport.request(responseHandler, HttpMethod.delete,
55+
'${_authSession.apiPath}$path', queryParams, body, headers);
56+
}
57+
58+
Future<SDKResponse<T>> post<T>(
59+
T Function(dynamic responseData, String contentType) responseHandler,
60+
String path,
61+
[dynamic queryParams,
62+
dynamic body,
63+
T responseInstance]) async {
64+
var headers = await _getHeaders();
65+
var requestBody = body == null ? null : jsonEncode(body);
66+
return _authSession.transport.request(responseHandler, HttpMethod.post,
67+
'${_authSession.apiPath}$path', queryParams, requestBody, headers);
68+
}
69+
70+
Future<SDKResponse<T>> put<T>(
71+
T Function(dynamic responseData, String contentType) responseHandler,
72+
String path,
73+
[dynamic queryParams,
74+
dynamic body,
75+
T responseInstance]) async {
76+
var headers = await _getHeaders();
77+
return _authSession.transport.request(responseHandler, HttpMethod.put,
78+
'${_authSession.apiPath}$path', queryParams, body, headers);
79+
}
80+
81+
Future<SDKResponse<T>> patch<T>(
82+
T Function(dynamic responseData, String contentType) responseHandler,
83+
String path,
84+
[dynamic queryParams,
85+
dynamic body,
86+
T responseInstance]) async {
87+
var headers = await _getHeaders();
88+
var requestBody;
89+
if (body != null) {
90+
body.removeWhere((key, value) => value == null);
91+
requestBody = jsonEncode(body);
92+
}
93+
return _authSession.transport.request(responseHandler, HttpMethod.patch,
94+
'${_authSession.apiPath}$path', queryParams, requestBody, headers);
95+
}
96+
97+
Future<Map<String, String>> _getHeaders() async {
98+
var headers = <String, String>{
99+
'x-looker-appid': _authSession.transport.settings.agentTag
100+
};
101+
headers.addAll(_authSession.authenticate());
102+
return headers;
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'constants.dart';
2+
3+
class ApiSettings {
4+
String _baseUrl;
5+
bool _verifySsl;
6+
int _timeout;
7+
String _agentTag;
8+
Function _credentialsCallback;
9+
10+
ApiSettings.fromMap(Map settings) {
11+
_baseUrl = settings.containsKey('base_url') ? settings['base_url'] : '';
12+
_verifySsl =
13+
settings.containsKey('verify_ssl') ? settings['verify_ssl'] : true;
14+
_timeout =
15+
settings.containsKey('timeout') ? settings['timeout'] : defaultTimeout;
16+
_agentTag = settings.containsKey('agent_tag')
17+
? settings['agent_tag']
18+
: '$agentPrefix $lookerVersion';
19+
_credentialsCallback = settings.containsKey(('credentials_callback'))
20+
? settings['credentials_callback']
21+
: null;
22+
}
23+
24+
bool isConfigured() {
25+
return _baseUrl != null;
26+
}
27+
28+
void readConfig(String section) {
29+
throw UnimplementedError('readConfig');
30+
}
31+
32+
String get version {
33+
return apiVersion;
34+
}
35+
36+
String get baseUrl {
37+
return _baseUrl;
38+
}
39+
40+
bool get verifySsl {
41+
return _verifySsl;
42+
}
43+
44+
int get timeout {
45+
return _timeout;
46+
}
47+
48+
String get agentTag {
49+
return _agentTag;
50+
}
51+
52+
Function get credentialsCallback {
53+
return _credentialsCallback;
54+
}
55+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class DelimList<T> {
2+
final List<T> _items;
3+
final String _separator;
4+
final String _prefix;
5+
final String _suffix;
6+
7+
DelimList(List<T> items,
8+
[String separator = ',', String prefix = '', String suffix = ''])
9+
: _items = items,
10+
_separator = separator,
11+
_prefix = prefix,
12+
_suffix = suffix;
13+
14+
@override
15+
String toString() {
16+
return '$_prefix${_items.join((_separator))}$_suffix';
17+
}
18+
}

0 commit comments

Comments
 (0)