labclient 0.3.5 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/labclient/branches/branch.rb +1 -1
  3. data/lib/labclient/client/helpers.rb +116 -0
  4. data/lib/labclient/client/meta.rb +81 -0
  5. data/lib/labclient/client/setup.rb +53 -0
  6. data/lib/labclient/client.rb +41 -162
  7. data/lib/labclient/common.rb +9 -4
  8. data/lib/labclient/docs.rb +6 -1
  9. data/lib/labclient/epics/issues/remove.rb +1 -1
  10. data/lib/labclient/epics/issues/update.rb +1 -1
  11. data/lib/labclient/error.rb +9 -0
  12. data/lib/labclient/feature_flags/list.rb +1 -1
  13. data/lib/labclient/files/show.rb +5 -5
  14. data/lib/labclient/generator/template_helper.rb +4 -3
  15. data/lib/labclient/generator/wizard.rb +11 -12
  16. data/lib/labclient/groups/group.rb +0 -2
  17. data/lib/labclient/groups/search.rb +2 -6
  18. data/lib/labclient/groups/stub.rb +1 -0
  19. data/lib/labclient/http.rb +27 -9
  20. data/lib/labclient/issues/issue.rb +1 -1
  21. data/lib/labclient/klass.rb +10 -11
  22. data/lib/labclient/lab_struct.rb +39 -7
  23. data/lib/labclient/logger.rb +50 -0
  24. data/lib/labclient/merge_requests/delete.rb +10 -2
  25. data/lib/labclient/merge_requests/merge_request.rb +1 -3
  26. data/lib/labclient/notifications/update.rb +1 -1
  27. data/lib/labclient/overview.rb +40 -2
  28. data/lib/labclient/paginated_response.rb +0 -2
  29. data/lib/labclient/pipelines/pipeline.rb +1 -1
  30. data/lib/labclient/projects/methods.rb +6 -2
  31. data/lib/labclient/projects/reference.rb +2 -2
  32. data/lib/labclient/projects/search.rb +2 -6
  33. data/lib/labclient/repository/repository_tree.rb +7 -0
  34. data/lib/labclient/repository/tree.rb +1 -1
  35. data/lib/labclient/version.rb +7 -1
  36. data/lib/labclient.rb +8 -2
  37. metadata +66 -18
@@ -4,6 +4,7 @@ module LabClient
4
4
  # Helper to Generate Data / Populate GitLab
5
5
  class Wizard
6
6
  include Generator::Names # Name Generator
7
+ include LabClient::Logger
7
8
  attr_reader :client
8
9
  attr_accessor :count, :random, :password, :templates, :domain, :skip_confirmation
9
10
 
@@ -26,7 +27,7 @@ module LabClient
26
27
  self.random = true # Populate Random or use only Templates
27
28
  self.count = default_count
28
29
  self.password = SecureRandom.uuid
29
- puts "Default Password: #{password}" unless client.quiet?
30
+ logger.info('Wizard Default Password', password: password) unless client.quiet?
30
31
 
31
32
  self.skip_confirmation = true
32
33
  self.domain = URI.parse(client.settings[:url]).hostname
@@ -54,11 +55,11 @@ module LabClient
54
55
  end
55
56
 
56
57
  def generate_users
57
- puts 'Generating Users' unless client.quiet?
58
+ logger.info 'Generating Users' unless client.quiet?
58
59
  @users = @user_names.map do |name|
59
60
  username = name.downcase.gsub(/[^0-9A-Za-z]/, '')
60
61
  email = "#{username}@#{domain}"
61
- puts "User -- Name: #{name}, UserName: #{username}, Email: #{email}" unless client.quiet?
62
+ logger.info('User', name: name, username: username, email: email) unless client.quiet?
62
63
 
63
64
  client.users.create(
64
65
  name: name,
@@ -71,41 +72,39 @@ module LabClient
71
72
  end
72
73
 
73
74
  def generate_groups
74
- puts 'Generating Groups' unless client.quiet?
75
+ logger.info 'Generating Groups' unless client.quiet?
75
76
  @groups = @group_names.map do |name|
76
77
  path = name.downcase.gsub(/[^0-9A-Za-z]/, '')
77
- puts "Group -- #{name}/#{path}" unless client.quiet?
78
+ logger.info "Group -- #{name}/#{path}" unless client.quiet?
78
79
  client.groups.create(name: name, path: path)
79
80
  end
80
81
  end
81
82
 
82
- # rubocop:disable Metrics/AbcSize
83
83
  def generate_group_membership
84
- puts 'Adding Group members' unless client.quiet?
84
+ logger.info 'Adding Group members' unless client.quiet?
85
85
  ## Group Access Level
86
86
  @groups.each do |group|
87
87
  @users.sample(rand(1..@users.count)).each do |user|
88
88
  level = group.valid_group_project_levels.sample
89
- puts "Group Add: #{group.name}: #{user.name} - #{level}" unless client.quiet?
89
+ logger.info('Group Add', name: group.name, user: user.name, level: level) unless client.quiet?
90
90
  group.member_add(user, access_level: level)
91
91
  # :nocov:
92
92
  rescue StandardError => e
93
- puts e.message unless client.quiet?
93
+ logger.fatal e.message unless client.quiet?
94
94
  next
95
95
  # :nocov:
96
96
  end
97
97
  end
98
98
  end
99
- # rubocop:enable Metrics/AbcSize
100
99
 
101
100
  def generate_projects(group)
102
- puts 'Generating Projects' unless client.quiet?
101
+ logger.info 'Generating Projects' unless client.quiet?
103
102
  # Collect Group Members
104
103
  members = group.members
105
104
 
106
105
  # Loop through project names, create project add issues
107
106
  @project_names.uniq.map do |project_name|
108
- puts "Project: #{project_name}" unless client.quiet?
107
+ logger.info "Project: #{project_name}" unless client.quiet?
109
108
  project = group.project_create(name: project_name, description: gen_description)
110
109
 
111
110
  rand(count[:issues]).times do
@@ -1,4 +1,3 @@
1
- # rubocop:disable Metrics/ClassLength
2
1
  # Top namespace
3
2
  module LabClient
4
3
  # Inspect Helper
@@ -344,4 +343,3 @@ module LabClient
344
343
  # rubocop:enable Metrics/BlockLength
345
344
  end
346
345
  end
347
- # rubocop:enable Metrics/ClassLength
@@ -70,20 +70,16 @@ module LabClient
70
70
 
71
71
  private
72
72
 
73
- # rubocop:disable Metrics/CyclomaticComplexity
74
- # TODO - Finish Classes
73
+ # TODO: - Finish Classes
75
74
  def klass_type(scope)
76
75
  case scope
77
76
  when :projects then Project
78
77
  when :issues then Issue
79
78
  when :merge_requests then MergeRequest
80
- when :wiki_blobs then nil # wiki_blobs
81
- when :milestones then nil # ::Project::MileStone
82
- when :blobs then nil # blobs
79
+ when :milestones, :wiki_blobs, :blobs then nil
83
80
  when :commits then Commit
84
81
  when :users then User
85
82
  end
86
83
  end
87
- # rubocop:enable Metrics/CyclomaticComplexity
88
84
  end
89
85
  end
@@ -3,6 +3,7 @@ module LabClient
3
3
  # Ugly Hack, but Makes Docs Easy
4
4
  class Groups < Common
5
5
  doc '' do
6
+ # Do Nothing!
6
7
  end
7
8
  end
8
9
  end
@@ -72,9 +72,9 @@ module Typhoeus
72
72
  attr_reader :path, :client
73
73
 
74
74
  def data
75
- return @data if @data
75
+ @data ||= process_body
76
76
 
77
- @data = process_body
77
+ @data
78
78
  end
79
79
 
80
80
  # Shim for CurlHelper
@@ -88,19 +88,37 @@ module Typhoeus
88
88
  elsif headers['content-type']&.include? 'text/plain'
89
89
  body
90
90
  else
91
- result = Oj.load(body, mode: :compat, object_class: LabClient::LabStruct)
91
+ result = Oj.load(body, mode: :compat, symbol_keys: true, object_class: LabClient::LabStruct)
92
92
  result.instance_variable_set(:@response, self) if result.instance_of?(LabClient::LabStruct)
93
93
  result
94
94
  end
95
95
  end
96
96
 
97
+ # Retry Helper Accessor
98
+ def retry?
99
+ code == 429
100
+ end
101
+
102
+ # Print Error information
103
+ # 1. Use Typheous `return_message` if there isn't any return body
104
+ # For network/uri/dns related issues
105
+ # 2. Use body for parsed responses
106
+ # For Bad Request, invalid params
107
+ # 3. Return raw data
108
+ # For non body responses
109
+ def find_friendly_error
110
+ case data
111
+ when nil
112
+ return_message
113
+ when LabClient::LabStruct
114
+ data[:message] || data[:error]
115
+ else # Handle String as well
116
+ data
117
+ end
118
+ end
119
+
97
120
  def friendly_error
98
- message = if data
99
- data[:message] || data[:error] || data
100
- else
101
- return_message
102
- end
103
- "#{code} - #{message}"
121
+ "#{code} - #{find_friendly_error}"
104
122
  end
105
123
  end
106
124
  end
@@ -12,7 +12,7 @@ module LabClient
12
12
  date_time_attrs %i[closed_at created_at updated_at]
13
13
 
14
14
  # User Fields
15
- user_attrs %i[closed_by author assignee]
15
+ user_attrs %i[author assignee]
16
16
 
17
17
  # Via State Events
18
18
  def close
@@ -2,9 +2,10 @@
2
2
  module LabClient
3
3
  # Common Configuration for all Class Helpers
4
4
  class Klass < LabStruct
5
+ include LabClient::Logger
5
6
  include CurlHelper
6
7
 
7
- attr_reader :client, :response
8
+ attr_reader :client
8
9
 
9
10
  extend Docs
10
11
 
@@ -15,7 +16,6 @@ module LabClient
15
16
 
16
17
  # API Methods here have to be explicitly documented / custom helpers
17
18
  # Assume no methods by default
18
- # rubocop:disable Metrics/AbcSize
19
19
  def help(help_filter = nil)
20
20
  docs = LabClient::Docs.docs.dig(group_name, 'Reference')
21
21
  unless docs
@@ -38,7 +38,6 @@ module LabClient
38
38
  # Ignore Output
39
39
  nil
40
40
  end
41
- # rubocop:enable Metrics/AbcSize
42
41
 
43
42
  # Documented API Methods
44
43
  def api_methods
@@ -101,18 +100,18 @@ module LabClient
101
100
  self
102
101
  end
103
102
 
104
- # rubocop:disable Lint/MissingSuper
105
- def initialize(hash = nil, response = nil, client = nil)
103
+ # Quiet Reader Helper
104
+ def quiet?
105
+ client.quiet?
106
+ end
107
+
108
+ def initialize(table = nil, response = nil, client = nil)
109
+ # @table = table unless table.nil?
106
110
  @client = client
107
111
  @response = response
108
112
 
109
- @table = {}
110
- hash&.each_pair do |k, v|
111
- k = k.to_sym
112
- @table[k] = v
113
- end
113
+ super(table)
114
114
  end
115
- # rubocop:enable Lint/MissingSuper
116
115
 
117
116
  # Forward response success
118
117
  def success?
@@ -1,16 +1,28 @@
1
- # Extensions for OpenStruct specific to LabClient
1
+ # Extensions for LabStruct specific to LabClient
2
2
  module LabClient
3
- # Unique inherited class to not override top level openstruct
4
- class LabStruct < OpenStruct
3
+ # Unique inherited class to not override top level LabStruct
4
+ class LabStruct
5
5
  include CurlHelper
6
- attr_reader :response
6
+ attr_reader :response, :table
7
+
8
+ def initialize(hash = {})
9
+ @table = if hash.instance_of?(LabClient::LabStruct)
10
+ hash.to_h
11
+ else
12
+ hash
13
+ end
14
+ end
15
+
16
+ def to_h
17
+ @table
18
+ end
7
19
 
8
20
  def keys
9
- to_h.keys.sort
21
+ @table.keys.sort
10
22
  end
11
23
 
12
24
  def inspect
13
- to_h.inspect
25
+ @table.inspect
14
26
  end
15
27
 
16
28
  def as_json(*args)
@@ -18,7 +30,7 @@ module LabClient
18
30
  end
19
31
 
20
32
  def slice(*opts)
21
- to_h.slice(*opts)
33
+ @table.slice(*opts)
22
34
  end
23
35
 
24
36
  def client
@@ -29,5 +41,25 @@ module LabClient
29
41
  def success?
30
42
  @response.success?
31
43
  end
44
+
45
+ def method_missing(method, *_args)
46
+ @table[method] if @table.keys.include?(method)
47
+ end
48
+
49
+ def respond_to_missing?(method_name, include_private = false)
50
+ @table.keys.include?(method_name) || super
51
+ end
52
+
53
+ def key?(idx)
54
+ @table.key? idx
55
+ end
56
+
57
+ def []=(name, value)
58
+ @table[name] = value
59
+ end
60
+
61
+ def [](name)
62
+ @table[name.to_sym]
63
+ end
32
64
  end
33
65
  end
@@ -0,0 +1,50 @@
1
+ # Top Namespace
2
+ module LabClient
3
+ # Helper to Unify Log Output
4
+ module Logger
5
+ def logger
6
+ LabClient::Logger.logger
7
+ end
8
+
9
+ def self.logger_setup
10
+ logger = Ougai::Logger.new($stdout)
11
+ logger.formatter = Ougai::Formatters::LabClient.new
12
+
13
+ logger
14
+ end
15
+
16
+ def self.logger
17
+ @logger ||= logger_setup
18
+ end
19
+ end
20
+ end
21
+
22
+ # Log Formatter for Readable Inline (Timestamp)
23
+ # https://github.com/eropple/ougai-formatters-inline_readable/blob/master/lib/ougai/formatters/inline_readable.rb
24
+ module Ougai
25
+ module Formatters
26
+ # LabClient Specific
27
+ class LabClient < Ougai::Formatters::Readable
28
+ # For Amazing Print
29
+ def ai_settings
30
+ { ruby19_syntax: true, multiline: false }
31
+ end
32
+
33
+ def time_format
34
+ '%Y-%m-%e %k:%M:%S %z'
35
+ end
36
+
37
+ def call(severity, time, _progname, data)
38
+ msg = data.delete(:msg)
39
+ @excluded_fields.each { |f| data.delete(f) }
40
+
41
+ level = @plain ? severity : colored_level(severity)
42
+ output = "[#{time.strftime(time_format)}] #{level}: #{msg}"
43
+
44
+ output += " #{data.ai(ai_settings)}" unless data.empty?
45
+
46
+ "#{output}\n"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -13,13 +13,21 @@ module LabClient
13
13
  end
14
14
 
15
15
  doc 'Delete' do
16
- desc 'Delete through MergeRequest'
16
+ desc 'Delete via MergeRequest'
17
17
  example <<~DOC
18
- mr = client.merge_requests.show(343,2)
18
+ mr = client.merge_requests.delete(343,2)
19
19
  mr.delete
20
20
  DOC
21
21
  end
22
22
 
23
+ doc 'Delete' do
24
+ desc 'Via Project'
25
+ example <<~DOC
26
+ project = client.projects.show(1)
27
+ project.merge_requests_delete(12)
28
+ DOC
29
+ end
30
+
23
31
  # Delete
24
32
  def delete(project_id, merge_request_id)
25
33
  project_id = format_id(project_id)
@@ -1,7 +1,6 @@
1
1
  # Top namespace
2
2
  module LabClient
3
3
  # Inspect Helper
4
- # rubocop:disable Metrics/ClassLength
5
4
  class MergeRequest < Klass
6
5
  include ClassHelpers
7
6
  # extend Docs
@@ -193,7 +192,7 @@ module LabClient
193
192
  Timeout.timeout(total_time) do
194
193
  loop do
195
194
  reload
196
- puts "Waiting for Merge Status: #{merge_status}"
195
+ logger.info "Waiting for Merge Status: #{merge_status}" unless quiet?
197
196
  break if %w[can_be_merged unchecked].include? merge_status
198
197
  raise "Cannot be merged! #{import_error}" if %w[cannot_be_merged cannot_be_merged_recheck].include? merge_status
199
198
 
@@ -252,5 +251,4 @@ module LabClient
252
251
 
253
252
  # rubocop:enable Metrics/BlockLength
254
253
  end
255
- # rubocop:enable Metrics/ClassLength
256
254
  end
@@ -31,7 +31,7 @@ module LabClient
31
31
  example 'client.notifications.update_project(16, level: :custom)'
32
32
 
33
33
  result <<~DOC
34
- OpenStruct {
34
+ LabStruct {
35
35
  :level => "custom",
36
36
  :events => LabStruct {
37
37
  :new_release => nil,
@@ -1,5 +1,4 @@
1
1
  # The glorious gitlab labclient of labs
2
- # rubocop:disable Metrics/ClassLength
3
2
  module LabClient
4
3
  # Helper for Docs
5
4
  class Overview
@@ -362,6 +361,30 @@ module LabClient
362
361
  DOC
363
362
  end
364
363
 
364
+ doc 'Other' do
365
+ title 'Retry'
366
+ markdown <<~DOC
367
+ Gitlab.com and other instances may have protected paths or rate limiting enabled. By default the LabClient will:
368
+
369
+ 1. Check if a `Retry Later` request was received (Return Code 429)
370
+ 2. Wait for combined `retry-after` and `delay_factor` value
371
+ 3. Retry until retries `max` is reached
372
+ DOC
373
+
374
+ example <<~DOC
375
+ client = LabClient::Client.new(
376
+ url: 'https://gitlab.labclient',
377
+ token: 'gitlab api token',
378
+ retry: {
379
+ max: 3, delay_factor: 2
380
+ }
381
+ )
382
+
383
+ # Or after the init
384
+ @client.settings[:retry] = { max: 3, delay_factor: 2 }
385
+ DOC
386
+ end
387
+
365
388
  doc 'Other' do
366
389
  title 'Quiet'
367
390
  desc 'Error messages by default are printed to STDOUT. This can be supressed via the :quiet setting'
@@ -378,6 +401,22 @@ module LabClient
378
401
  DOC
379
402
  end
380
403
 
404
+ doc 'Other' do
405
+ title 'Debug'
406
+ desc 'Print Request Information'
407
+
408
+ example <<~DOC
409
+ client = LabClient::Client.new(
410
+ url: 'https://gitlab.labclient',
411
+ token: 'gitlab api token',
412
+ debug: true
413
+ )
414
+
415
+ # Or after the init
416
+ client.settings[:debug] = true
417
+ DOC
418
+ end
419
+
381
420
  doc 'Other' do
382
421
  title 'Curl'
383
422
  desc 'Sometimes you just wana have a curl example to test or send someone'
@@ -428,4 +467,3 @@ module LabClient
428
467
  end
429
468
  end
430
469
  end
431
- # rubocop:enable Metrics/ClassLength
@@ -40,9 +40,7 @@ module LabClient
40
40
  DOC
41
41
  end
42
42
 
43
- # rubocop:disable Lint/MissingSuper
44
43
  def respond_to_missing?(method_name, include_private = false); end
45
- # rubocop:enable Lint/MissingSuper
46
44
 
47
45
  def each_page(&block)
48
46
  yield @array # This will eventually be the whole list
@@ -63,7 +63,7 @@ module LabClient
63
63
  Timeout.timeout(total_time) do
64
64
  loop do
65
65
  reload
66
- puts "Waiting for Pipeline: #{status}"
66
+ logger.info('Waiting for Pipeline', status: status) unless quiet?
67
67
  break if %w[skipped manual canceled success].include? status
68
68
  raise 'Pipeline failed' if status == 'failed'
69
69
 
@@ -321,7 +321,7 @@ module LabClient
321
321
  end
322
322
 
323
323
  # Files
324
- def file(file_path, ref = :master, kind = nil)
324
+ def file(file_path, ref = :main, kind = nil)
325
325
  client.files.show(id, file_path, ref, kind)
326
326
  end
327
327
 
@@ -647,6 +647,10 @@ module LabClient
647
647
  client.merge_requests.create(id, query)
648
648
  end
649
649
 
650
+ def merge_request_delete(merge_request_iid)
651
+ client.merge_requests.delete(id, merge_request_iid)
652
+ end
653
+
650
654
  # Access Requests
651
655
  def request_access
652
656
  client.projects.access_requests.create(id)
@@ -748,7 +752,7 @@ module LabClient
748
752
  Timeout.timeout(total_time) do
749
753
  loop do
750
754
  reload
751
- puts "Waiting for Import Status: #{import_status}"
755
+ logger.info('Waiting for Import Status', status: import_status) unless quiet?
752
756
  break if %w[none finished].include? import_status
753
757
  raise "Import Failed: #{import_error}" if import_status == 'failed'
754
758
 
@@ -1,6 +1,6 @@
1
1
  # Top namespace
2
2
  module LabClient
3
- # rubocop:disable Metrics/BlockLength, Metrics/ClassLength
3
+ # rubocop:disable Metrics/BlockLength
4
4
  # Help Do Block
5
5
  class Project < Klass
6
6
  help do
@@ -268,5 +268,5 @@ module LabClient
268
268
  option 'parent', 'Show (API Call) Project Parent, returns either Group or User'
269
269
  end
270
270
  end
271
- # rubocop:enable Metrics/BlockLength, Metrics/ClassLength
271
+ # rubocop:enable Metrics/BlockLength
272
272
  end
@@ -72,20 +72,16 @@ module LabClient
72
72
 
73
73
  private
74
74
 
75
- # rubocop:disable Metrics/CyclomaticComplexity
76
- # TODO - Finish Classes
75
+ # TODO: - Finish Classes
77
76
  def klass_type(scope)
78
77
  case scope
79
78
  when :issues then Issue
80
79
  when :merge_requests then MergeRequest
81
80
  when :notes then Note
82
- when :wiki_blobs then nil # wiki_blobs
83
- when :milestones then nil # ::Project::MileStone
84
- when :blobs then nil # blobs
81
+ when :wiki_blobs, :milestones, :blobs then nil
85
82
  when :commits then Commit
86
83
  when :users then User
87
84
  end
88
85
  end
89
- # rubocop:enable Metrics/CyclomaticComplexity
90
86
  end
91
87
  end
@@ -0,0 +1,7 @@
1
+ # Top namespace
2
+ module LabClient
3
+ # Inspect Helper
4
+ class RepositoryTree < Klass
5
+ include ClassHelpers
6
+ end
7
+ end
@@ -29,7 +29,7 @@ module LabClient
29
29
  def tree(project_id, query = {})
30
30
  project_id = format_id(project_id)
31
31
 
32
- client.request(:get, "projects/#{project_id}/repository/tree", nil, query)
32
+ client.request(:get, "projects/#{project_id}/repository/tree", RepositoryTree, query)
33
33
  end
34
34
  end
35
35
  end
@@ -1,3 +1,9 @@
1
+ # Overall version
1
2
  module LabClient
2
- VERSION = '0.3.5'.freeze
3
+ VERSION = '0.5.1'.freeze
4
+
5
+ # Make it easy to print version
6
+ def self.version
7
+ VERSION
8
+ end
3
9
  end
data/lib/labclient.rb CHANGED
@@ -5,8 +5,10 @@ require 'active_support/all'
5
5
  require 'amazing_print'
6
6
  require 'gitlab_chronic_duration'
7
7
  require 'ostruct'
8
+ require 'ougai'
8
9
 
9
10
  require 'labclient/version'
11
+ require 'labclient/logger'
10
12
  require 'labclient/docs'
11
13
  require 'labclient/curl'
12
14
  require 'labclient/http'
@@ -769,6 +771,7 @@ require 'labclient/pipelines/pipeline'
769
771
  # Repository
770
772
  require 'labclient/repository/alias'
771
773
  require 'labclient/repository/tree'
774
+ require 'labclient/repository/repository_tree'
772
775
  require 'labclient/repository/blob'
773
776
  require 'labclient/repository/archive'
774
777
  require 'labclient/repository/compare'
@@ -874,7 +877,10 @@ require 'labclient/generator/generator'
874
877
  require 'labclient/generator/wizard'
875
878
 
876
879
  # Dynamically Require Templates (Simplify new template creation)
877
- Dir["#{File.dirname(__FILE__)}/labclient/generator/templates/*.rb"].sort.each { |file| require file }
880
+ Dir["#{File.dirname(__FILE__)}/labclient/generator/templates/*.rb"].each { |file| require file }
878
881
 
879
- # I am Very Last
882
+ # Load Client Files - I am Very Last!
883
+ require 'labclient/client/setup'
884
+ require 'labclient/client/helpers'
885
+ require 'labclient/client/meta'
880
886
  require 'labclient/client'