t-tech-investments 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cursor/rules/git-flow.mdc +49 -5
- data/.cursor/rules/sdk-architecture.mdc +30 -0
- data/.cursor/rules/sdk-code-standards.mdc +22 -0
- data/.cursor/rules/sdk-patterns.mdc +22 -0
- data/.cursor/rules/sdk-testing.mdc +19 -0
- data/CHANGELOG.md +4 -0
- data/README.md +195 -4
- data/Rakefile +4 -0
- data/contracts.lock +6 -0
- data/lib/t/tech/investments/client.rb +70 -0
- data/lib/t/tech/investments/coercers.rb +171 -0
- data/lib/t/tech/investments/configuration.rb +74 -0
- data/lib/t/tech/investments/errors.rb +24 -0
- data/lib/t/tech/investments/proto/common_pb.rb +42 -0
- data/lib/t/tech/investments/proto/google/api/field_behavior_pb.rb +19 -0
- data/lib/t/tech/investments/proto/instruments_pb.rb +157 -0
- data/lib/t/tech/investments/proto/instruments_services_pb.rb +115 -0
- data/lib/t/tech/investments/proto/marketdata_pb.rb +99 -0
- data/lib/t/tech/investments/proto/marketdata_services_pb.rb +68 -0
- data/lib/t/tech/investments/proto/operations_pb.rb +78 -0
- data/lib/t/tech/investments/proto/operations_services_pb.rb +67 -0
- data/lib/t/tech/investments/proto/orders_pb.rb +64 -0
- data/lib/t/tech/investments/proto/orders_services_pb.rb +69 -0
- data/lib/t/tech/investments/proto/sandbox_pb.rb +37 -0
- data/lib/t/tech/investments/proto/sandbox_services_pb.rb +75 -0
- data/lib/t/tech/investments/proto/signals_pb.rb +37 -0
- data/lib/t/tech/investments/proto/signals_services_pb.rb +36 -0
- data/lib/t/tech/investments/proto/stoporders_pb.rb +45 -0
- data/lib/t/tech/investments/proto/stoporders_services_pb.rb +38 -0
- data/lib/t/tech/investments/proto/users_pb.rb +47 -0
- data/lib/t/tech/investments/proto/users_services_pb.rb +51 -0
- data/lib/t/tech/investments/proto_loader.rb +49 -0
- data/lib/t/tech/investments/services/market_data_facade.rb +35 -0
- data/lib/t/tech/investments/services/market_data_stream_session.rb +148 -0
- data/lib/t/tech/investments/services/registry.rb +38 -0
- data/lib/t/tech/investments/services/unary_adapter.rb +64 -0
- data/lib/t/tech/investments/services.rb +6 -0
- data/lib/t/tech/investments/transport.rb +137 -0
- data/lib/t/tech/investments/version.rb +1 -1
- data/lib/t/tech/investments.rb +30 -1
- data/script/regenerate_proto +119 -0
- data/sig/t/tech/investments.rbs +114 -1
- metadata +67 -4
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module T
|
|
4
|
+
module Tech
|
|
5
|
+
module Investments
|
|
6
|
+
# Loads generated proto files so that Service/Stub classes and messages are available.
|
|
7
|
+
# Adds proto directory to $LOAD_PATH and requires contract files in dependency order.
|
|
8
|
+
module ProtoLoader
|
|
9
|
+
PROTO_DIR = File.expand_path("proto", __dir__)
|
|
10
|
+
|
|
11
|
+
# Contract _pb.rb (messages); order respects dependencies.
|
|
12
|
+
PB_FILES = %w[
|
|
13
|
+
common_pb
|
|
14
|
+
instruments_pb
|
|
15
|
+
marketdata_pb
|
|
16
|
+
operations_pb
|
|
17
|
+
orders_pb
|
|
18
|
+
sandbox_pb
|
|
19
|
+
signals_pb
|
|
20
|
+
stoporders_pb
|
|
21
|
+
users_pb
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# Service definitions (depend on _pb).
|
|
25
|
+
SERVICES_FILES = %w[
|
|
26
|
+
users_services_pb
|
|
27
|
+
instruments_services_pb
|
|
28
|
+
marketdata_services_pb
|
|
29
|
+
operations_services_pb
|
|
30
|
+
orders_services_pb
|
|
31
|
+
sandbox_services_pb
|
|
32
|
+
signals_services_pb
|
|
33
|
+
stoporders_services_pb
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
def load!
|
|
39
|
+
return if @loaded
|
|
40
|
+
|
|
41
|
+
$LOAD_PATH.unshift(PROTO_DIR) unless $LOAD_PATH.include?(PROTO_DIR)
|
|
42
|
+
PB_FILES.each { |f| require f }
|
|
43
|
+
SERVICES_FILES.each { |f| require f }
|
|
44
|
+
@loaded = true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module T
|
|
4
|
+
module Tech
|
|
5
|
+
module Investments
|
|
6
|
+
module Services
|
|
7
|
+
# Combines unary MarketData RPCs and #stream for MarketDataStream bidi.
|
|
8
|
+
# client.market_data.get_candles(...) -> unary; client.market_data.stream { |s| ... } -> stream session.
|
|
9
|
+
class MarketDataFacade
|
|
10
|
+
def initialize(unary_adapter, transport)
|
|
11
|
+
@unary_adapter = unary_adapter
|
|
12
|
+
@transport = transport
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stream(timeout: nil, &block)
|
|
16
|
+
session = MarketDataStreamSession.new(@transport, timeout: timeout)
|
|
17
|
+
block&.call(session)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
|
21
|
+
if @unary_adapter.respond_to?(method_name)
|
|
22
|
+
@unary_adapter.public_send(method_name, *args, **kwargs, &block)
|
|
23
|
+
else
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
29
|
+
@unary_adapter.respond_to?(method_name, include_private) || super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module T
|
|
4
|
+
module Tech
|
|
5
|
+
module Investments
|
|
6
|
+
module Services
|
|
7
|
+
# Session for MarketDataStream bidi: queue of outgoing requests, each_event yields
|
|
8
|
+
# Ruby-friendly events (ping ignored). Uses Coercers for request building and response mapping.
|
|
9
|
+
# Reconciliation: call get_my_subscriptions then resubscribe or restart stream as needed.
|
|
10
|
+
# Contract (Tinkoff::...) is resolved at runtime after ProtoLoader.load!
|
|
11
|
+
# rubocop:disable Metrics/ClassLength
|
|
12
|
+
class MarketDataStreamSession
|
|
13
|
+
CONTRACT_NS = "Tinkoff::Public::Invest::Api::Contract::V1"
|
|
14
|
+
|
|
15
|
+
REQUEST_SPECS = {
|
|
16
|
+
subscribe_candles: %i[SubscribeCandlesRequest subscribe_candles_request],
|
|
17
|
+
subscribe_order_book: %i[SubscribeOrderBookRequest subscribe_order_book_request],
|
|
18
|
+
subscribe_trades: %i[SubscribeTradesRequest subscribe_trades_request],
|
|
19
|
+
subscribe_info: %i[SubscribeInfoRequest subscribe_info_request],
|
|
20
|
+
subscribe_last_price: %i[SubscribeLastPriceRequest subscribe_last_price_request],
|
|
21
|
+
get_my_subscriptions: %i[GetMySubscriptions get_my_subscriptions]
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# Response payload field name -> event type (we skip :ping)
|
|
25
|
+
RESPONSE_PAYLOAD_TO_TYPE = {
|
|
26
|
+
subscribe_candles_response: :subscribe_candles_response,
|
|
27
|
+
subscribe_order_book_response: :subscribe_order_book_response,
|
|
28
|
+
subscribe_trades_response: :subscribe_trades_response,
|
|
29
|
+
subscribe_info_response: :subscribe_info_response,
|
|
30
|
+
candle: :candle,
|
|
31
|
+
trade: :trade,
|
|
32
|
+
orderbook: :orderbook,
|
|
33
|
+
trading_status: :trading_status,
|
|
34
|
+
ping: :ping,
|
|
35
|
+
subscribe_last_price_response: :subscribe_last_price_response,
|
|
36
|
+
last_price: :last_price,
|
|
37
|
+
open_interest: :open_interest
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
def initialize(transport, timeout: nil)
|
|
41
|
+
@transport = transport
|
|
42
|
+
@timeout = timeout
|
|
43
|
+
@queue = Queue.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def subscribe_candles(**kwargs)
|
|
47
|
+
push_request(:subscribe_candles, kwargs)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def subscribe_order_book(**kwargs)
|
|
51
|
+
push_request(:subscribe_order_book, kwargs)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def subscribe_trades(**kwargs)
|
|
55
|
+
push_request(:subscribe_trades, kwargs)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def subscribe_info(**kwargs)
|
|
59
|
+
push_request(:subscribe_info, kwargs)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def subscribe_last_price(**kwargs)
|
|
63
|
+
push_request(:subscribe_last_price, kwargs)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def get_my_subscriptions # rubocop:disable Naming/AccessorMethodName
|
|
67
|
+
push_request(:get_my_subscriptions, {})
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Iterates server responses as events (Hash with :type and :data). Ignores ping.
|
|
71
|
+
# rubocop:disable Metrics/MethodLength
|
|
72
|
+
# Request stream is fed from the queue (subscribe_* / get_my_subscriptions pushed earlier).
|
|
73
|
+
def each_event(&block)
|
|
74
|
+
return to_enum(:each_event) unless block
|
|
75
|
+
|
|
76
|
+
request_enum = build_request_enum
|
|
77
|
+
stream_stub = @transport.stub(contract::MarketDataStreamService::Stub)
|
|
78
|
+
response_enum = @transport.bidi_stream(
|
|
79
|
+
stream_stub, :market_data_stream, request_enum, timeout: @timeout
|
|
80
|
+
)
|
|
81
|
+
response_enum.each do |response|
|
|
82
|
+
event = response_to_event(response)
|
|
83
|
+
next if event.nil?
|
|
84
|
+
|
|
85
|
+
block.call(event)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
# rubocop:enable Metrics/MethodLength
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def contract
|
|
93
|
+
@contract ||= Object.const_get(CONTRACT_NS)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def push_request(kind, kwargs)
|
|
97
|
+
msg_name, oneof_key = REQUEST_SPECS.fetch(kind)
|
|
98
|
+
msg_class = contract.const_get(msg_name)
|
|
99
|
+
inner = kwargs.empty? ? msg_class.new : Coercers.to_request(msg_class, kwargs)
|
|
100
|
+
req = contract::MarketDataRequest.new(oneof_key => inner)
|
|
101
|
+
@queue.push(req)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_request_enum
|
|
105
|
+
Enumerator.new do |y|
|
|
106
|
+
loop { y.yield @queue.pop }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def response_to_event(response)
|
|
111
|
+
payload_type, payload = extract_payload(response)
|
|
112
|
+
return nil if payload_type.nil? || payload_type == :ping
|
|
113
|
+
|
|
114
|
+
event_type = RESPONSE_PAYLOAD_TO_TYPE[payload_type]
|
|
115
|
+
data = payload ? Coercers.to_hash(payload, ruby_friendly: true) : {}
|
|
116
|
+
{ type: event_type, data: data }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
120
|
+
def extract_payload(response)
|
|
121
|
+
if response.respond_to?(:which_oneof)
|
|
122
|
+
field = response.which_oneof("payload")
|
|
123
|
+
if field
|
|
124
|
+
field_sym = field.to_sym
|
|
125
|
+
val = response.public_send(field_sym)
|
|
126
|
+
return [field_sym, val] if val
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
RESPONSE_PAYLOAD_TO_TYPE.each_key do |f|
|
|
130
|
+
next unless response.respond_to?(f)
|
|
131
|
+
|
|
132
|
+
val = response.public_send(f)
|
|
133
|
+
next if val.nil?
|
|
134
|
+
return [f, val] if protobuf_message?(val) || val.to_s.to_s.strip != ""
|
|
135
|
+
end
|
|
136
|
+
[nil, nil]
|
|
137
|
+
end
|
|
138
|
+
# rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
139
|
+
|
|
140
|
+
def protobuf_message?(val)
|
|
141
|
+
val.respond_to?(:descriptor) && val.respond_to?(:to_h)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
# rubocop:enable Metrics/ClassLength
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module T
|
|
4
|
+
module Tech
|
|
5
|
+
module Investments
|
|
6
|
+
module Services
|
|
7
|
+
# Maps SDK service names to gRPC Service classes. Used by Client to build adapters.
|
|
8
|
+
# Resolves constants after proto is loaded (no Tinkoff at require time).
|
|
9
|
+
module Registry
|
|
10
|
+
CONTRACT_NS = "Tinkoff::Public::Invest::Api::Contract::V1"
|
|
11
|
+
|
|
12
|
+
SERVICE_CLASS_PATHS = {
|
|
13
|
+
users: "#{CONTRACT_NS}::UsersService::Service",
|
|
14
|
+
instruments: "#{CONTRACT_NS}::InstrumentsService::Service",
|
|
15
|
+
market_data: "#{CONTRACT_NS}::MarketDataService::Service",
|
|
16
|
+
market_data_stream: "#{CONTRACT_NS}::MarketDataStreamService::Service",
|
|
17
|
+
operations: "#{CONTRACT_NS}::OperationsService::Service",
|
|
18
|
+
orders: "#{CONTRACT_NS}::OrdersService::Service",
|
|
19
|
+
sandbox: "#{CONTRACT_NS}::SandboxService::Service",
|
|
20
|
+
stop_orders: "#{CONTRACT_NS}::StopOrdersService::Service",
|
|
21
|
+
signals: "#{CONTRACT_NS}::SignalService::Service"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def service_class(name)
|
|
27
|
+
path = SERVICE_CLASS_PATHS.fetch(name) { raise KeyError, "Unknown service: #{name.inspect}" }
|
|
28
|
+
Object.const_get(path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def service_names
|
|
32
|
+
SERVICE_CLASS_PATHS.keys
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module T
|
|
4
|
+
module Tech
|
|
5
|
+
module Investments
|
|
6
|
+
module Services
|
|
7
|
+
# Adapter that exposes unary RPCs from a gRPC Service class via Transport + Coercers.
|
|
8
|
+
# Methods are defined dynamically from service_class.rpc_descs (no streaming in this adapter).
|
|
9
|
+
class UnaryAdapter
|
|
10
|
+
attr_reader :service_class, :transport, :stub
|
|
11
|
+
|
|
12
|
+
def initialize(service_class, transport)
|
|
13
|
+
@service_class = service_class
|
|
14
|
+
@transport = transport
|
|
15
|
+
@stub = transport.stub(service_class.rpc_stub_class)
|
|
16
|
+
define_unary_methods!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def define_unary_methods!
|
|
22
|
+
service_class.rpc_descs.each_value do |desc|
|
|
23
|
+
next unless unary?(desc)
|
|
24
|
+
|
|
25
|
+
method_sym = rpc_name_to_snake(desc.name)
|
|
26
|
+
request_class = desc.input
|
|
27
|
+
response_class = desc.output
|
|
28
|
+
define_unary_method(method_sym, request_class, response_class)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unary?(desc)
|
|
33
|
+
!stream_type?(desc.input) && !stream_type?(desc.output)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stream_type?(val)
|
|
37
|
+
val.is_a?(::GRPC::RpcDesc::Stream)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def rpc_name_to_snake(name)
|
|
41
|
+
name.to_s
|
|
42
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
43
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
44
|
+
.downcase
|
|
45
|
+
.to_sym
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def define_unary_method(method_sym, request_class, _response_class)
|
|
49
|
+
return if singleton_class.method_defined?(method_sym)
|
|
50
|
+
|
|
51
|
+
adapter = self
|
|
52
|
+
define_singleton_method(method_sym) do |**kwargs, &_block|
|
|
53
|
+
raw = kwargs.delete(:raw)
|
|
54
|
+
timeout = kwargs.delete(:timeout)
|
|
55
|
+
request = Coercers.to_request(request_class, kwargs)
|
|
56
|
+
response = adapter.transport.unary(adapter.stub, method_sym, request, timeout: timeout)
|
|
57
|
+
raw ? response : Coercers.to_hash(response, ruby_friendly: true)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "grpc"
|
|
4
|
+
|
|
5
|
+
module T
|
|
6
|
+
module Tech
|
|
7
|
+
module Investments
|
|
8
|
+
# gRPC transport: channel, metadata, deadlines, unary calls, error mapping.
|
|
9
|
+
# No business logic; Services call Transport, not stubs directly.
|
|
10
|
+
class Transport
|
|
11
|
+
TRACKING_ID_KEY = "x-tracking-id"
|
|
12
|
+
|
|
13
|
+
attr_reader :config
|
|
14
|
+
|
|
15
|
+
def initialize(config_snapshot)
|
|
16
|
+
@config = config_snapshot
|
|
17
|
+
@channel = nil
|
|
18
|
+
@stubs = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns a stub for the given stub class (e.g. UsersService::Stub).
|
|
22
|
+
# Stubs are cached per class. Channel uses TLS for default endpoint.
|
|
23
|
+
def stub(stub_class)
|
|
24
|
+
@stubs[stub_class] ||= stub_class.new(
|
|
25
|
+
config.endpoint,
|
|
26
|
+
channel_credentials,
|
|
27
|
+
channel_args: {}
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Performs a unary RPC: request metadata, deadline, call stub, map GRPC::BadStatus → Errors::GrpcError.
|
|
32
|
+
# @param stub_instance [Object] gRPC stub (from #stub)
|
|
33
|
+
# @param method_name [Symbol] e.g. :get_accounts
|
|
34
|
+
# @param request [Object] protobuf request message
|
|
35
|
+
# @param timeout [Integer, nil] optional override in seconds
|
|
36
|
+
# @return [Object] protobuf response message
|
|
37
|
+
def unary(stub_instance, method_name, request, timeout: nil)
|
|
38
|
+
opts = request_options(timeout)
|
|
39
|
+
stub_instance.public_send(method_name, request, opts)
|
|
40
|
+
rescue GRPC::BadStatus => e
|
|
41
|
+
raise map_bad_status(e)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Bidirectional stream: sends request_enum, returns response Enumerator.
|
|
45
|
+
# Maps GRPC::BadStatus when iterating responses; extracts x-tracking-id from metadata.
|
|
46
|
+
# @param stub_instance [Object] gRPC stub (from #stub)
|
|
47
|
+
# @param method_name [Symbol] e.g. :market_data_stream
|
|
48
|
+
# @param request_enum [Enumerator] yields protobuf request messages
|
|
49
|
+
# @param timeout [Integer, nil] optional override in seconds
|
|
50
|
+
# @return [Enumerator] yields protobuf response messages; raises Errors::GrpcError on stream error
|
|
51
|
+
def bidi_stream(stub_instance, method_name, request_enum, timeout: nil)
|
|
52
|
+
opts = request_options(timeout)
|
|
53
|
+
response_enum = stub_instance.public_send(method_name, request_enum, opts)
|
|
54
|
+
wrap_response_enum(response_enum)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def close
|
|
58
|
+
@channel&.close
|
|
59
|
+
@channel = nil
|
|
60
|
+
@stubs.clear
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def channel_credentials
|
|
66
|
+
return @channel_credentials if defined?(@channel_credentials) && @channel_credentials
|
|
67
|
+
|
|
68
|
+
certs_path = config.ssl_ca_certs.to_s.strip
|
|
69
|
+
if certs_path.empty?
|
|
70
|
+
@channel_credentials = GRPC::Core::ChannelCredentials.new
|
|
71
|
+
return @channel_credentials
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
pem = read_ssl_ca_certs(certs_path)
|
|
75
|
+
@channel_credentials = GRPC::Core::ChannelCredentials.new(pem)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def read_ssl_ca_certs(path)
|
|
79
|
+
pem = File.read(path)
|
|
80
|
+
if pem.to_s.strip.empty?
|
|
81
|
+
raise Errors::ConfigurationError, "SSL CA certs file is empty: #{path}"
|
|
82
|
+
end
|
|
83
|
+
pem
|
|
84
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
85
|
+
raise Errors::ConfigurationError, "SSL CA certs file is not readable: #{path} (#{e.class})"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def request_options(timeout)
|
|
89
|
+
deadline = Time.now + (timeout || config.timeout)
|
|
90
|
+
{
|
|
91
|
+
metadata: request_metadata,
|
|
92
|
+
deadline: deadline
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def request_metadata
|
|
97
|
+
meta = { "authorization" => "Bearer #{config.token}" }
|
|
98
|
+
meta["x-app-name"] = config.app_name unless config.app_name.to_s.empty?
|
|
99
|
+
meta
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def map_bad_status(err)
|
|
103
|
+
tracking_id = extract_tracking_id(err)
|
|
104
|
+
Errors::GrpcError.new(
|
|
105
|
+
err.message.to_s,
|
|
106
|
+
code: err.code,
|
|
107
|
+
details: err.details.to_s,
|
|
108
|
+
tracking_id: tracking_id
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def extract_tracking_id(bad_status)
|
|
113
|
+
return nil unless bad_status.respond_to?(:metadata) && bad_status.metadata
|
|
114
|
+
|
|
115
|
+
meta = normalized_metadata_hash(bad_status.metadata)
|
|
116
|
+
return nil unless meta.is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
meta[TRACKING_ID_KEY] || meta[TRACKING_ID_KEY.tr("-", "_")] || meta[:x_tracking_id]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def normalized_metadata_hash(raw)
|
|
122
|
+
return nil unless raw.is_a?(Hash)
|
|
123
|
+
|
|
124
|
+
raw.key?(:metadata) ? raw[:metadata] : raw
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def wrap_response_enum(response_enum)
|
|
128
|
+
Enumerator.new do |y|
|
|
129
|
+
response_enum.each { |msg| y.yield msg }
|
|
130
|
+
rescue GRPC::BadStatus => e
|
|
131
|
+
raise map_bad_status(e)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
data/lib/t/tech/investments.rb
CHANGED
|
@@ -6,7 +6,36 @@ module T
|
|
|
6
6
|
module Tech
|
|
7
7
|
module Investments
|
|
8
8
|
class Error < StandardError; end
|
|
9
|
-
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require_relative "investments/errors"
|
|
14
|
+
require_relative "investments/configuration"
|
|
15
|
+
require_relative "investments/transport"
|
|
16
|
+
require_relative "investments/coercers"
|
|
17
|
+
require_relative "investments/proto_loader"
|
|
18
|
+
require_relative "investments/services"
|
|
19
|
+
require_relative "investments/client"
|
|
20
|
+
|
|
21
|
+
module T
|
|
22
|
+
module Tech
|
|
23
|
+
module Investments
|
|
24
|
+
class << self
|
|
25
|
+
attr_accessor :configuration
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self.configuration = Configuration.from_env
|
|
29
|
+
|
|
30
|
+
def self.configure
|
|
31
|
+
yield configuration if block_given?
|
|
32
|
+
configuration
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.client(overrides = {})
|
|
36
|
+
config = configuration.merge(overrides)
|
|
37
|
+
Client.new(config.snapshot)
|
|
38
|
+
end
|
|
10
39
|
end
|
|
11
40
|
end
|
|
12
41
|
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Regenerates Ruby protobuf and gRPC code from invest-contracts.
|
|
5
|
+
# Reads contracts.lock for repository and ref, then runs grpc_tools_ruby_protoc.
|
|
6
|
+
# Run from project root: bundle exec ruby script/regenerate_proto
|
|
7
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
8
|
+
|
|
9
|
+
require "english"
|
|
10
|
+
require "json"
|
|
11
|
+
require "fileutils"
|
|
12
|
+
require "open3"
|
|
13
|
+
|
|
14
|
+
PROJECT_ROOT = File.expand_path("..", __dir__)
|
|
15
|
+
CONTRACTS_LOCK = File.join(PROJECT_ROOT, "contracts.lock")
|
|
16
|
+
OUTPUT_DIR = File.join(PROJECT_ROOT, "lib", "t", "tech", "investments", "proto")
|
|
17
|
+
TMP_REPO = File.join(PROJECT_ROOT, "tmp", "investAPI")
|
|
18
|
+
|
|
19
|
+
def load_lock
|
|
20
|
+
data = JSON.parse(File.read(CONTRACTS_LOCK))
|
|
21
|
+
[data["repository"], data["ref"], File.join(data["contracts_path"] || "src/docs/contracts", "")]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def clone_or_fetch(repo_url, ref)
|
|
25
|
+
FileUtils.mkdir_p(File.dirname(TMP_REPO))
|
|
26
|
+
if Dir.exist?(TMP_REPO)
|
|
27
|
+
system("git", "-C", TMP_REPO, "fetch", "origin", "tag", ref, "--no-tags", out: File::NULL, err: File::NULL) ||
|
|
28
|
+
system("git", "-C", TMP_REPO, "fetch", "origin", ref, out: File::NULL, err: File::NULL)
|
|
29
|
+
else
|
|
30
|
+
system("git", "clone", "--depth", "1", "--no-single-branch", repo_url, TMP_REPO,
|
|
31
|
+
out: File::NULL, err: File::NULL) ||
|
|
32
|
+
system("git", "clone", repo_url, TMP_REPO, out: File::NULL, err: File::NULL)
|
|
33
|
+
return false unless $CHILD_STATUS.success?
|
|
34
|
+
|
|
35
|
+
system("git", "-C", TMP_REPO, "fetch", "origin", ref,
|
|
36
|
+
out: File::NULL, err: File::NULL)
|
|
37
|
+
end
|
|
38
|
+
system("git", "-C", TMP_REPO, "checkout", ref, out: File::NULL, err: File::NULL)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def proto_include_path
|
|
42
|
+
grpc_tools = Gem::Specification.find_by_name("grpc-tools")
|
|
43
|
+
base = grpc_tools.full_gem_path
|
|
44
|
+
%w[include src].each do |sub|
|
|
45
|
+
path = File.join(base, sub)
|
|
46
|
+
ts = File.join(path, "google", "protobuf", "timestamp.proto")
|
|
47
|
+
return path if File.file?(ts)
|
|
48
|
+
end
|
|
49
|
+
nil
|
|
50
|
+
rescue Gem::MissingSpecError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def run_protoc(contracts_dir, extra_include)
|
|
55
|
+
FileUtils.rm_rf(OUTPUT_DIR)
|
|
56
|
+
FileUtils.mkdir_p(OUTPUT_DIR)
|
|
57
|
+
|
|
58
|
+
proto_files = Dir[File.join(contracts_dir, "**", "*.proto")].sort
|
|
59
|
+
proto_files.reject! { |f| f.include?("google/protobuf/") } # skip if any vendored
|
|
60
|
+
if proto_files.empty?
|
|
61
|
+
# Flat list in contracts_path only
|
|
62
|
+
contracts_path = File.dirname(contracts_dir.chomp("/"))
|
|
63
|
+
proto_files = Dir[File.join(contracts_path, "*.proto")].sort
|
|
64
|
+
proto_files += Dir[File.join(contracts_path, "google", "api", "*.proto")].sort
|
|
65
|
+
contracts_dir = contracts_path
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
args = ["-I", contracts_dir]
|
|
69
|
+
args += ["-I", extra_include] if extra_include
|
|
70
|
+
args += ["--ruby_out", OUTPUT_DIR, "--grpc_out", OUTPUT_DIR]
|
|
71
|
+
args += proto_files
|
|
72
|
+
|
|
73
|
+
stdout, stderr, status = Open3.capture3("bundle", "exec", "grpc_tools_ruby_protoc", *args, chdir: PROJECT_ROOT)
|
|
74
|
+
unless status.success?
|
|
75
|
+
warn stderr
|
|
76
|
+
warn stdout
|
|
77
|
+
return false
|
|
78
|
+
end
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def main
|
|
83
|
+
Dir.chdir(PROJECT_ROOT)
|
|
84
|
+
|
|
85
|
+
unless File.file?(CONTRACTS_LOCK)
|
|
86
|
+
warn "contracts.lock not found at #{CONTRACTS_LOCK}"
|
|
87
|
+
exit 1
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
repo_url, ref, contracts_subpath = load_lock
|
|
91
|
+
contracts_dir = File.join(TMP_REPO, contracts_subpath.to_s.chomp("/"))
|
|
92
|
+
contracts_dir = File.join(TMP_REPO, "src/docs/contracts") unless Dir.exist?(contracts_dir)
|
|
93
|
+
|
|
94
|
+
puts "Repository: #{repo_url} @ #{ref}"
|
|
95
|
+
puts "Contracts: #{contracts_dir}"
|
|
96
|
+
|
|
97
|
+
unless clone_or_fetch(repo_url, ref)
|
|
98
|
+
warn "Failed to clone/fetch repository"
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
unless Dir.exist?(contracts_dir)
|
|
103
|
+
warn "Contracts directory not found: #{contracts_dir}"
|
|
104
|
+
exit 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
extra_include = proto_include_path
|
|
108
|
+
puts "Output: #{OUTPUT_DIR}"
|
|
109
|
+
|
|
110
|
+
unless run_protoc(contracts_dir, extra_include)
|
|
111
|
+
warn "protoc failed"
|
|
112
|
+
exit 1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
puts "Done. Generated files in #{OUTPUT_DIR}"
|
|
116
|
+
end
|
|
117
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
118
|
+
|
|
119
|
+
main
|