DhanHQ 3.4.0 → 4.0.0
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/CHANGELOG.md +22 -1
- data/GUIDE.md +16 -0
- data/README.md +28 -0
- data/docs/CONFIGURATION.md +25 -0
- data/lib/DhanHQ/client.rb +76 -26
- data/lib/DhanHQ/configuration.rb +26 -24
- data/lib/DhanHQ/core/base_api.rb +3 -2
- data/lib/DhanHQ/dry_run/simulator.rb +3 -2
- data/lib/DhanHQ/helpers/request_helper.rb +13 -3
- data/lib/DhanHQ/resources/market_feed.rb +1 -1
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/base_connection.rb +2 -1
- data/lib/DhanHQ/ws/client.rb +7 -5
- data/lib/DhanHQ/ws/market_depth/client.rb +4 -3
- data/lib/DhanHQ/ws/orders/client.rb +4 -4
- data/lib/DhanHQ/ws/orders/connection.rb +1 -1
- data/lib/dhan_hq.rb +74 -46
- metadata +22 -22
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c2f0784210b7f48ccb056b8cc486ce5c3ef655f69a4bd2834a150b1d04a8bc7
|
|
4
|
+
data.tar.gz: a6628fc83e8ec61bd4ca4b0eb6147a67232156f98ab49196c0a81850f9b4ac59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0a4147017f7722d9a74cec3105fb57e9b8710142abda6c04d5948caa7500b21ce8c31958cc501e99643a31afffe5e64846b5a73ff239f1a8fb87074e57a6856
|
|
7
|
+
data.tar.gz: 508f55621273349e9060e941cc9bf8b7cef7fb28befb8026b57b977ba0954760f783eb5dbb26c19593ac113232c12fa74884fcd57a81b61dd21dbc3d66c7b9e9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [4.0.0] - 2026-08-31
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Multi-Instance Client Isolation (`DhanHQ::Client.new`)** — run multiple isolated client instances in the same process with separate configurations, credentials, sandbox environments, and rate limiters. Ideal for multi-tenant applications, managing multiple trading accounts concurrently, or comparing sandbox vs. production environments side-by-side.
|
|
6
|
+
```ruby
|
|
7
|
+
client_a = DhanHQ::Client.new(client_id: "ACC_A", access_token: "TOKEN_A")
|
|
8
|
+
client_b = DhanHQ::Client.new(client_id: "ACC_B", access_token: "TOKEN_B", sandbox: true)
|
|
9
|
+
|
|
10
|
+
orders_a = client_a.orders.all
|
|
11
|
+
orders_b = client_b.orders.all
|
|
12
|
+
```
|
|
13
|
+
- **Instance-bound Resource Accessors** — direct accessors on `DhanHQ::Client` instances (`client.orders`, `client.positions`, `client.holdings`, `client.funds`, `client.trades`, `client.market_feed`, `client.option_chain`, `client.historical_data`, `client.forever_orders`, `client.alert_orders`, `client.iceberg_orders`, `client.super_orders`, `client.twap_orders`, `client.statements`, `client.kill_switch`, `client.pnl_exit`, `client.trader_control`, `client.ip_setup`, `client.edis`) bound to that client's configuration and routed to matching rate-limiting tiers (`client_for(API_TYPE)`).
|
|
14
|
+
- **Instance-bound WebSocket Helpers** — `client.ws_market_feed`, `client.ws_order_update`, and `client.ws_market_depth` configured automatically with the client instance's credentials and URLs.
|
|
15
|
+
- **Keyword Argument Initialization** — `DhanHQ::Client.new` accepts keyword configuration parameters (`dry_run: true`, `sandbox: true`, `client_id: "..."`, `access_token: "..."`) or a `Configuration` instance.
|
|
16
|
+
- **Per-Instance Dry-Run Mode** — `DhanHQ::Client.new(dry_run: true)` simulates state-changing writes locally for that instance regardless of the global configuration.
|
|
17
|
+
- **Dynamic Model Resource Invalidation** — `DhanHQ.reset_model_resources!` automatically refreshes class-level model caches (`Models::Order.resource`, etc.) whenever `DhanHQ.configure`, `configure_with_env`, `configuration=`, or `reset_configuration!` is invoked.
|
|
18
|
+
- **Per-Client Auto Token Management** — `client.enable_auto_token_management!(dhan_client_id: ..., pin: ..., totp_secret: ...)` for automated TOTP-based token lifecycle management per client instance.
|
|
19
|
+
|
|
1
20
|
## [3.4.0] - 2026-08-15
|
|
2
21
|
|
|
3
22
|
### Added
|
|
@@ -19,7 +38,9 @@
|
|
|
19
38
|
### Fixed
|
|
20
39
|
|
|
21
40
|
- **`docs/RAILS_INTEGRATION.md`'s Sidekiq examples (§5, §6) called `client.wait!`, `client.subscribe(array)`, and `DhanHQ::WS::Client.new(kind: :order_updates)`** — none of which exist. `DhanHQ::WS::Client`/`DhanHQ::WS::Orders::Client` have no blocking wait method at all; `Client#start` spawns a background thread and returns immediately. Replaced with the real API (`DhanHQ::WS.connect`/`DhanHQ::WS::Orders.connect` plus a `connected?`-based liveness loop), guarded by a new spec that locks down the method names these examples call by name.
|
|
22
|
-
-
|
|
41
|
+
- **Every runtime dependency except `faraday` had no version floor at all** (`add_dependency "x"`, equivalent to `>= 0` — satisfied by any version ever published). Found while investigating whether the Ruby floor could drop to 3.1 (it can't — see below): forcing a fresh `bundle install` under Ruby 3.1.6 resolved `dry-validation` to `0.4.1`, a 2016-era, pre-`Dry::Validation::Contract` API generation every contract in `lib/DhanHQ/contracts/` is incompatible with — every contract spec failed at `require`-time. Pinning that alone wasn't enough: with `dry-validation` fixed, the same fresh resolve then pulled `activesupport` down to `7.2.3.2` (ActiveSupport 8.x itself requires Ruby ≥ 3.2), and AS 7-vs-8 behavioral differences broke `Agent::ToolRegistry`, `MCP::Server`, and `Risk::Pipeline` — 63 failures with no connection to Ruby version syntax at all.
|
|
42
|
+
|
|
43
|
+
All ten now have an explicit floor matching their currently locked major (`activesupport >= 8.0`, `dry-validation ~> 1.0`, and so on) — closing the class of bug, not just the two instances that happened to surface. The `dry-validation` constraint went through two attempts: the first pass in this same release pinned it to `~> 1.11`, which — because `~>` locks to the *last* segment given — actually meant `>= 1.11, < 1.12` and would have silently blocked every future 1.12+ release. Corrected to `~> 1.0`, which excludes the ancient `0.x` line (the actual problem) without capping legitimate 1.x updates.
|
|
23
44
|
- **`spec/dhan_hq/contracts/expired_options_data_contract_spec.rb` and `expired_options_data_spec.rb` hardcoded `from_date: "2021-08-02"`**, which is itself now more than 5 years in the past and so failed the contract's own "cannot be more than 5 years ago" rule — the shared fixture failed the exact rule it existed to exercise, cascading into every test merged onto it. Replaced every literal 2021 date with one computed relative to `Date.today`, preserving each test's original intent (span length, ordering, the exact-31-day boundary).
|
|
24
45
|
|
|
25
46
|
### Investigated, not changed
|
data/GUIDE.md
CHANGED
|
@@ -77,6 +77,22 @@ For token rotation without restarting the app, set `access_token_provider` (Proc
|
|
|
77
77
|
|
|
78
78
|
See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md) and README “Dynamic access token”.
|
|
79
79
|
|
|
80
|
+
**Multi-instance clients (multi-account support)**
|
|
81
|
+
|
|
82
|
+
When you need to trade across multiple accounts, run multi-tenant setups, or isolate sandbox/testing environments in a single process, use `DhanHQ::Client.new`:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# Account 1
|
|
86
|
+
client1 = DhanHQ::Client.new(client_id: "ACC_1", access_token: "TOKEN_1")
|
|
87
|
+
|
|
88
|
+
# Account 2 (Sandbox)
|
|
89
|
+
client2 = DhanHQ::Client.new(client_id: "ACC_2", access_token: "TOKEN_2", sandbox: true)
|
|
90
|
+
|
|
91
|
+
# Resource accessors directly bound to client instance:
|
|
92
|
+
orders1 = client1.orders.all
|
|
93
|
+
orders2 = client2.orders.all
|
|
94
|
+
```
|
|
95
|
+
|
|
80
96
|
---
|
|
81
97
|
|
|
82
98
|
## Working With Models
|
data/README.md
CHANGED
|
@@ -218,6 +218,34 @@ end
|
|
|
218
218
|
|
|
219
219
|
When the API returns 401, the client retries **once** with a fresh token from your provider.
|
|
220
220
|
|
|
221
|
+
### Multi-Instance Clients (Multi-Account / Multi-Tenant)
|
|
222
|
+
|
|
223
|
+
For multi-tenant applications, managing multiple trading accounts concurrently, or isolating sandbox from production within the same process, construct explicit `DhanHQ::Client` instances:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
# Account 1 (Live Trading)
|
|
227
|
+
client1 = DhanHQ::Client.new(
|
|
228
|
+
client_id: "ACC_1_ID",
|
|
229
|
+
access_token: "ACC_1_TOKEN"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Account 2 (Sandbox / Testing)
|
|
233
|
+
client2 = DhanHQ::Client.new(
|
|
234
|
+
client_id: "ACC_2_ID",
|
|
235
|
+
access_token: "ACC_2_TOKEN",
|
|
236
|
+
sandbox: true,
|
|
237
|
+
dry_run: true
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Resource accessors are bound to the client instance:
|
|
241
|
+
positions = client1.positions.all
|
|
242
|
+
orders = client2.orders.all
|
|
243
|
+
|
|
244
|
+
# Instance-bound WebSocket streams:
|
|
245
|
+
ws_stream = client1.ws_market_feed(mode: :ticker) { |tick| puts tick[:ltp] }
|
|
246
|
+
ws_orders = client1.ws_order_update
|
|
247
|
+
```
|
|
248
|
+
|
|
221
249
|
> **Full details**: TOTP flows, partner mode, token endpoint bootstrap, auto-management — see [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md).
|
|
222
250
|
|
|
223
251
|
---
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -106,6 +106,31 @@ end
|
|
|
106
106
|
- **`access_token_provider`**: Callable (Proc/lambda) returning the token string. Called on every request (no memoization). When set, the gem uses it instead of `access_token`.
|
|
107
107
|
- **`on_token_expired`**: Optional callable invoked when a 401/token-expired triggers a **single retry** (only when `access_token_provider` is set).
|
|
108
108
|
|
|
109
|
+
## Multi-Instance Client Configuration
|
|
110
|
+
|
|
111
|
+
For multi-account or multi-tenant architectures, construct explicit `DhanHQ::Client` instances without depending on global singleton state:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
# Direct configuration via keyword arguments
|
|
115
|
+
client = DhanHQ::Client.new(
|
|
116
|
+
client_id: "YOUR_CLIENT_ID",
|
|
117
|
+
access_token: "YOUR_ACCESS_TOKEN",
|
|
118
|
+
sandbox: false,
|
|
119
|
+
dry_run: false
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Or via an explicit Configuration object
|
|
123
|
+
custom_config = DhanHQ::Configuration.new do |c|
|
|
124
|
+
c.client_id = "TENANT_CLIENT_ID"
|
|
125
|
+
c.access_token = "TENANT_ACCESS_TOKEN"
|
|
126
|
+
c.dry_run = true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
client = DhanHQ::Client.new(config: custom_config)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Each client instance manages its own Faraday connection, isolated configuration, dry-run state, and instance-bound resource accessors (`client.orders`, `client.positions`, `client.market_feed`, etc.).
|
|
133
|
+
|
|
109
134
|
For detailed authentication flows, see [AUTHENTICATION.md](AUTHENTICATION.md).
|
|
110
135
|
|
|
111
136
|
## Available Resources
|
data/lib/DhanHQ/client.rb
CHANGED
|
@@ -42,42 +42,68 @@ module DhanHQ
|
|
|
42
42
|
].freeze
|
|
43
43
|
private_constant :RETRYABLE_TRANSPORT_ERRORS
|
|
44
44
|
|
|
45
|
-
attr_reader :token_manager
|
|
45
|
+
attr_reader :config, :token_manager, :api_type
|
|
46
46
|
|
|
47
|
-
# Initializes a new DhanHQ Client.
|
|
48
|
-
#
|
|
49
|
-
# Establishes state and checks its one invariant; the HTTP connection is opened
|
|
50
|
-
# lazily on first use, so constructing a client does no network setup.
|
|
51
|
-
#
|
|
52
|
-
# @example Create a new client:
|
|
53
|
-
# client = DhanHQ::Client.new(api_type: :order_api)
|
|
47
|
+
# Initializes a new DhanHQ Client with instance-level configuration and rate limiting.
|
|
54
48
|
#
|
|
49
|
+
# @param config_or_attrs [DhanHQ::Configuration, Hash, nil] Optional configuration or attribute hash
|
|
50
|
+
# @param config [DhanHQ::Configuration, nil] Optional keyword configuration
|
|
55
51
|
# @param api_type [Symbol] Type of API (`:order_api`, `:data_api`, `:non_trading_api`)
|
|
56
|
-
# @
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Shared per API type, so separate clients coordinate against one limit.
|
|
52
|
+
# @param attrs [Hash] Additional keyword configuration attributes
|
|
53
|
+
def initialize(config_or_attrs = nil, config: nil, api_type: :order_api, **attrs)
|
|
54
|
+
@config = resolve_config(config_or_attrs, config, attrs)
|
|
55
|
+
@api_type = api_type
|
|
61
56
|
@rate_limiter = RateLimiter.for(api_type)
|
|
62
57
|
|
|
63
58
|
raise DhanHQ::Error, "RateLimiter initialization failed" unless @rate_limiter
|
|
64
59
|
end
|
|
65
60
|
|
|
66
61
|
# The Faraday connection used for HTTP requests.
|
|
67
|
-
#
|
|
68
|
-
# Built on first use and rebuilt whenever the configured base URL changes — which
|
|
69
|
-
# happens when sandbox mode is toggled, or when a token endpoint hands back a
|
|
70
|
-
# different host mid-process.
|
|
62
|
+
# Built on first use and rebuilt whenever the configured base URL changes.
|
|
71
63
|
#
|
|
72
64
|
# @return [Faraday::Connection]
|
|
73
65
|
def connection
|
|
74
|
-
current_url =
|
|
66
|
+
current_url = @config.base_url
|
|
75
67
|
return @connection if @connection && @last_base_url == current_url
|
|
76
68
|
|
|
77
69
|
@last_base_url = current_url
|
|
78
70
|
@connection = build_connection(current_url)
|
|
79
71
|
end
|
|
80
72
|
|
|
73
|
+
# Resource accessors bound to this client instance. Each resource gets the client
|
|
74
|
+
# matching its own API_TYPE, so e.g. option chain calls throttle at 1 per 3 sec
|
|
75
|
+
# instead of borrowing the order client's 10/sec limiter.
|
|
76
|
+
def orders = @orders ||= Resources::Orders.new(client: client_for(Resources::Orders::API_TYPE))
|
|
77
|
+
def market_feed = @market_feed ||= Resources::MarketFeed.new(client: client_for(Resources::MarketFeed::API_TYPE))
|
|
78
|
+
def positions = @positions ||= Resources::Positions.new(client: client_for(Resources::Positions::API_TYPE))
|
|
79
|
+
def holdings = @holdings ||= Resources::Holdings.new(client: client_for(Resources::Holdings::API_TYPE))
|
|
80
|
+
def historical_data = @historical_data ||= Resources::HistoricalData.new(client: client_for(Resources::HistoricalData::API_TYPE))
|
|
81
|
+
def option_chain = @option_chain ||= Resources::OptionChain.new(client: client_for(Resources::OptionChain::API_TYPE))
|
|
82
|
+
def funds = @funds ||= Resources::Funds.new(client: client_for(Resources::Funds::API_TYPE))
|
|
83
|
+
def trades = @trades ||= Resources::Trades.new(client: client_for(Resources::Trades::API_TYPE))
|
|
84
|
+
def forever_orders = @forever_orders ||= Resources::ForeverOrders.new(client: client_for(Resources::ForeverOrders::API_TYPE))
|
|
85
|
+
def alert_orders = @alert_orders ||= Resources::AlertOrders.new(client: client_for(Resources::AlertOrders::API_TYPE))
|
|
86
|
+
def iceberg_orders = @iceberg_orders ||= Resources::IcebergOrders.new(client: client_for(Resources::IcebergOrders::API_TYPE))
|
|
87
|
+
def super_orders = @super_orders ||= Resources::SuperOrders.new(client: client_for(Resources::SuperOrders::API_TYPE))
|
|
88
|
+
def twap_orders = @twap_orders ||= Resources::TwapOrders.new(client: client_for(Resources::TwapOrders::API_TYPE))
|
|
89
|
+
def statements = @statements ||= Resources::Statements.new(client: client_for(Resources::Statements::API_TYPE))
|
|
90
|
+
def kill_switch = @kill_switch ||= Resources::KillSwitch.new(client: client_for(Resources::KillSwitch::API_TYPE))
|
|
91
|
+
def pnl_exit = @pnl_exit ||= Resources::PnlExit.new(client: client_for(Resources::PnlExit::API_TYPE))
|
|
92
|
+
def trader_control = @trader_control ||= Resources::TraderControl.new(client: client_for(Resources::TraderControl::API_TYPE))
|
|
93
|
+
def ip_setup = @ip_setup ||= Resources::IPSetup.new(client: client_for(Resources::IPSetup::API_TYPE))
|
|
94
|
+
def edis = @edis ||= Resources::Edis.new(client: client_for(Resources::Edis::API_TYPE))
|
|
95
|
+
|
|
96
|
+
# WebSocket streams bound to this client instance configuration
|
|
97
|
+
def ws_market_feed(mode: :ticker, url: nil, &block)
|
|
98
|
+
WS::Client.new(mode: mode, url: url, config: @config).tap do |ws|
|
|
99
|
+
ws.start if block_given?
|
|
100
|
+
ws.on(:tick, &block) if block_given?
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def ws_order_update = WS::Orders::Client.new(config: @config)
|
|
105
|
+
def ws_market_depth = WS::MarketDepth::Client.new(config: @config)
|
|
106
|
+
|
|
81
107
|
# Sends an HTTP request to the API with automatic retry for transient errors.
|
|
82
108
|
#
|
|
83
109
|
# @param method [Symbol] The HTTP method (`:get`, `:post`, `:put`, `:delete`)
|
|
@@ -93,8 +119,6 @@ module DhanHQ
|
|
|
93
119
|
@token_manager&.ensure_valid_token!
|
|
94
120
|
@rate_limiter.throttle!
|
|
95
121
|
|
|
96
|
-
# A non-idempotent write is not safely retryable: the API has no idempotency
|
|
97
|
-
# key, so a request that timed out may already have reached the exchange.
|
|
98
122
|
effective_retries = retryable_write?(method, path) ? retries : 0
|
|
99
123
|
|
|
100
124
|
with_auth_retry do
|
|
@@ -112,10 +136,9 @@ module DhanHQ
|
|
|
112
136
|
yield
|
|
113
137
|
rescue DhanHQ::InvalidAuthenticationError, DhanHQ::InvalidTokenError,
|
|
114
138
|
DhanHQ::TokenExpiredError, DhanHQ::AuthenticationFailedError => e
|
|
115
|
-
|
|
116
|
-
raise unless config&.access_token_provider
|
|
139
|
+
raise unless @config&.access_token_provider
|
|
117
140
|
|
|
118
|
-
config.on_token_expired&.call(e)
|
|
141
|
+
@config.on_token_expired&.call(e)
|
|
119
142
|
DhanHQ.logger&.warn("[DhanHQ::Client] Auth failure (#{e.class}), retrying once with fresh token")
|
|
120
143
|
yield
|
|
121
144
|
end
|
|
@@ -218,20 +241,47 @@ module DhanHQ
|
|
|
218
241
|
@token_manager.generate!
|
|
219
242
|
end
|
|
220
243
|
|
|
244
|
+
# A client sharing this one's config but rate-limited for a different API tier.
|
|
245
|
+
# RateLimiter.for(api_type) is a shared instance per type, so sibling clients still
|
|
246
|
+
# throttle against the same counters as any other client using that tier.
|
|
247
|
+
#
|
|
248
|
+
# @param api_type [Symbol] Target API tier (:order_api, :data_api, :non_trading_api, :option_chain)
|
|
249
|
+
# @return [DhanHQ::Client]
|
|
250
|
+
def client_for(api_type)
|
|
251
|
+
return self if api_type == @api_type
|
|
252
|
+
|
|
253
|
+
@sibling_clients ||= {}
|
|
254
|
+
@sibling_clients[api_type] ||= self.class.new(config: @config, api_type: api_type)
|
|
255
|
+
end
|
|
256
|
+
|
|
221
257
|
private
|
|
222
258
|
|
|
259
|
+
def resolve_config(config_or_attrs, keyword_config, attrs = {})
|
|
260
|
+
if config_or_attrs.is_a?(DhanHQ::Configuration)
|
|
261
|
+
config_or_attrs
|
|
262
|
+
elsif config_or_attrs.is_a?(Hash)
|
|
263
|
+
DhanHQ::Configuration.new(config_or_attrs.merge(attrs))
|
|
264
|
+
elsif keyword_config
|
|
265
|
+
keyword_config
|
|
266
|
+
elsif attrs.any?
|
|
267
|
+
DhanHQ::Configuration.new(attrs)
|
|
268
|
+
else
|
|
269
|
+
DhanHQ.ensure_configuration!
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
223
273
|
# Simulator that answers state-changing requests locally when dry run is on.
|
|
224
274
|
#
|
|
225
275
|
# @return [DhanHQ::DryRun::Simulator]
|
|
226
276
|
def dry_run
|
|
227
|
-
@dry_run ||= DhanHQ::DryRun::Simulator.new
|
|
277
|
+
@dry_run ||= DhanHQ::DryRun::Simulator.new(config: @config)
|
|
228
278
|
end
|
|
229
279
|
|
|
230
280
|
# A write may be auto-retried only when the user has explicitly opted in.
|
|
231
281
|
def retryable_write?(method, path)
|
|
232
282
|
return true unless WritePaths.mutating?(method, path)
|
|
233
283
|
|
|
234
|
-
|
|
284
|
+
@config&.retry_non_idempotent_writes? || false
|
|
235
285
|
end
|
|
236
286
|
|
|
237
287
|
# Fills in a +correlationId+ on order placements that lack one, so a caller who
|
|
@@ -240,7 +290,7 @@ module DhanHQ
|
|
|
240
290
|
def with_correlation_id(method, path, payload)
|
|
241
291
|
return payload unless WritePaths.order_placement?(method, path)
|
|
242
292
|
return payload unless payload.is_a?(Hash)
|
|
243
|
-
return payload unless
|
|
293
|
+
return payload unless @config&.auto_correlation_id?
|
|
244
294
|
return payload if correlation_id?(payload)
|
|
245
295
|
|
|
246
296
|
inject_correlation_id(payload)
|
data/lib/DhanHQ/configuration.rb
CHANGED
|
@@ -212,30 +212,32 @@ module DhanHQ
|
|
|
212
212
|
@ws_debug == true
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@
|
|
223
|
-
@
|
|
224
|
-
@
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
@
|
|
231
|
-
@
|
|
232
|
-
@
|
|
233
|
-
@
|
|
234
|
-
@
|
|
235
|
-
@
|
|
236
|
-
@
|
|
237
|
-
@
|
|
238
|
-
@
|
|
215
|
+
def initialize(attrs = {})
|
|
216
|
+
@client_id = attrs[:client_id] || ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
217
|
+
@access_token = attrs[:access_token] || ENV.fetch("DHAN_ACCESS_TOKEN", nil)
|
|
218
|
+
@access_token_provider = attrs[:access_token_provider]
|
|
219
|
+
@on_token_expired = attrs[:on_token_expired]
|
|
220
|
+
@sandbox = attrs.key?(:sandbox) ? attrs[:sandbox] : env_flag("DHAN_SANDBOX", default: false)
|
|
221
|
+
@dry_run = attrs.key?(:dry_run) ? attrs[:dry_run] : env_flag("DHAN_DRY_RUN", default: false)
|
|
222
|
+
@retry_non_idempotent_writes = attrs.key?(:retry_non_idempotent_writes) ? attrs[:retry_non_idempotent_writes] : env_flag("DHAN_RETRY_WRITES", default: false)
|
|
223
|
+
@auto_correlation_id = attrs.key?(:auto_correlation_id) ? attrs[:auto_correlation_id] : env_flag("DHAN_AUTO_CORRELATION_ID", default: false)
|
|
224
|
+
@warn_on_ambiguous_write_failure = if attrs.key?(:warn_on_ambiguous_write_failure)
|
|
225
|
+
attrs[:warn_on_ambiguous_write_failure]
|
|
226
|
+
else
|
|
227
|
+
env_flag("DHAN_WARN_AMBIGUOUS_WRITE_FAILURE",
|
|
228
|
+
default: true)
|
|
229
|
+
end
|
|
230
|
+
@base_url = attrs[:base_url] || ENV.fetch("DHAN_BASE_URL", nil)
|
|
231
|
+
@ws_version = (attrs[:ws_version] || ENV.fetch("DHAN_WS_VERSION", 2)).to_i
|
|
232
|
+
@ws_order_url = attrs[:ws_order_url] || ENV.fetch("DHAN_WS_ORDER_URL", nil)
|
|
233
|
+
@ws_market_feed_url = attrs[:ws_market_feed_url] || ENV.fetch("DHAN_WS_MARKET_FEED_URL", nil)
|
|
234
|
+
@ws_market_depth_url = attrs[:ws_market_depth_url] || ENV.fetch("DHAN_WS_MARKET_DEPTH_URL", nil)
|
|
235
|
+
@market_depth_level = (attrs[:market_depth_level] || ENV.fetch("DHAN_MARKET_DEPTH_LEVEL", "20")).to_i
|
|
236
|
+
@ws_debug = attrs.key?(:ws_debug) ? attrs[:ws_debug] : env_flag("DHAN_WS_DEBUG", default: false)
|
|
237
|
+
@ws_user_type = attrs[:ws_user_type] || ENV.fetch("DHAN_WS_USER_TYPE", "SELF")
|
|
238
|
+
@partner_id = attrs[:partner_id] || ENV.fetch("DHAN_PARTNER_ID", nil)
|
|
239
|
+
@partner_secret = attrs[:partner_secret] || ENV.fetch("DHAN_PARTNER_SECRET", nil)
|
|
240
|
+
yield(self) if block_given?
|
|
239
241
|
end
|
|
240
242
|
|
|
241
243
|
private
|
data/lib/DhanHQ/core/base_api.rb
CHANGED
|
@@ -17,9 +17,10 @@ module DhanHQ
|
|
|
17
17
|
|
|
18
18
|
# Initializes the BaseAPI with the appropriate Client instance
|
|
19
19
|
#
|
|
20
|
+
# @param client [DhanHQ::Client, nil] Optional injected client instance
|
|
20
21
|
# @param api_type [Symbol] API type (`:order_api`, `:data_api`, `:non_trading_api`)
|
|
21
|
-
def initialize(api_type: self.class::API_TYPE)
|
|
22
|
-
@client = DhanHQ::Client.new(api_type: api_type)
|
|
22
|
+
def initialize(client: nil, api_type: self.class::API_TYPE)
|
|
23
|
+
@client = client || DhanHQ::Client.new(api_type: api_type)
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
# Perform a GET request via `Client`
|
|
@@ -25,8 +25,9 @@ module DhanHQ
|
|
|
25
25
|
# Extracts a fabricated id out of a request path.
|
|
26
26
|
ID_PATTERN = /#{Regexp.escape(ID_PREFIX)}[A-Za-z0-9]+/
|
|
27
27
|
|
|
28
|
-
def initialize(ledger: Ledger.instance)
|
|
28
|
+
def initialize(ledger: Ledger.instance, config: nil)
|
|
29
29
|
@ledger = ledger
|
|
30
|
+
@config = config
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# Whether this request should be answered locally.
|
|
@@ -35,7 +36,7 @@ module DhanHQ
|
|
|
35
36
|
# @param path [String, nil] Request path
|
|
36
37
|
# @return [Boolean]
|
|
37
38
|
def simulates?(method, path)
|
|
38
|
-
return false unless DhanHQ.configuration&.dry_run?
|
|
39
|
+
return false unless (@config || DhanHQ.configuration)&.dry_run?
|
|
39
40
|
|
|
40
41
|
WritePaths.mutating?(method, path) || replayable_read?(method, path)
|
|
41
42
|
end
|
|
@@ -21,6 +21,16 @@ module DhanHQ
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
+
def current_config
|
|
25
|
+
if respond_to?(:config) && config
|
|
26
|
+
config
|
|
27
|
+
elsif respond_to?(:client) && client.respond_to?(:config) && client.config
|
|
28
|
+
client.config
|
|
29
|
+
else
|
|
30
|
+
DhanHQ.configuration
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
24
34
|
# Dynamically builds headers for each request.
|
|
25
35
|
#
|
|
26
36
|
# @param path [String] The API endpoint path.
|
|
@@ -41,7 +51,7 @@ module DhanHQ
|
|
|
41
51
|
|
|
42
52
|
# Add client-id for DATA APIs (now including sandbox profile/funds)
|
|
43
53
|
if data_api?(path)
|
|
44
|
-
client_id =
|
|
54
|
+
client_id = current_config&.client_id
|
|
45
55
|
unless client_id
|
|
46
56
|
raise DhanHQ::InvalidAuthenticationError,
|
|
47
57
|
"client_id is required for DATA APIs but not set. Please configure DhanHQ with DHAN_CLIENT_ID."
|
|
@@ -53,7 +63,7 @@ module DhanHQ
|
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
def resolved_access_token
|
|
56
|
-
|
|
66
|
+
current_config&.resolved_access_token
|
|
57
67
|
end
|
|
58
68
|
|
|
59
69
|
# Determines if the API path requires a `client-id` header.
|
|
@@ -80,7 +90,7 @@ module DhanHQ
|
|
|
80
90
|
|
|
81
91
|
out = payload
|
|
82
92
|
if path && %i[post put patch].include?(method)
|
|
83
|
-
client_id =
|
|
93
|
+
client_id = current_config&.client_id
|
|
84
94
|
needs_client_id = data_api?(path) || payload_requires_dhan_client_id?(path)
|
|
85
95
|
if client_id && needs_client_id && !payload.key?(:dhanClientId) && !payload.key?("dhanClientId")
|
|
86
96
|
out = payload.dup
|
data/lib/DhanHQ/version.rb
CHANGED
|
@@ -23,8 +23,9 @@ module DhanHQ
|
|
|
23
23
|
# @param options [Hash] Connection options
|
|
24
24
|
# @option options [Integer] :max_backoff Maximum backoff time (default: 90)
|
|
25
25
|
# @option options [Integer] :cool_off_429 Cool off time for 429 errors (default: 60)
|
|
26
|
-
def initialize(url:, **options)
|
|
26
|
+
def initialize(url:, config: nil, **options)
|
|
27
27
|
@url = url
|
|
28
|
+
@config = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
|
|
28
29
|
@callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
|
|
29
30
|
@started = Concurrent::AtomicBoolean.new(false)
|
|
30
31
|
@stop = false
|
data/lib/DhanHQ/ws/client.rb
CHANGED
|
@@ -20,7 +20,8 @@ module DhanHQ
|
|
|
20
20
|
class Client
|
|
21
21
|
# @param mode [Symbol] Feed mode (:ticker, :quote, :full).
|
|
22
22
|
# @param url [String, nil] Optional custom WebSocket endpoint.
|
|
23
|
-
|
|
23
|
+
# @param config [DhanHQ::Configuration, nil] Optional configuration instance.
|
|
24
|
+
def initialize(mode: :ticker, url: nil, config: nil)
|
|
24
25
|
@mode = mode # :ticker, :quote, :full (adjust to your API)
|
|
25
26
|
@bus = CmdBus.new
|
|
26
27
|
@state = SubState.new
|
|
@@ -29,13 +30,14 @@ module DhanHQ
|
|
|
29
30
|
@last_message_at = Concurrent::AtomicReference.new(nil)
|
|
30
31
|
@connected_at = Concurrent::AtomicReference.new(nil)
|
|
31
32
|
@reconnects = Concurrent::AtomicFixnum.new(0)
|
|
33
|
+
@config = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
|
|
32
34
|
|
|
33
|
-
token =
|
|
35
|
+
token = @config.resolved_access_token
|
|
34
36
|
raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
|
|
35
37
|
|
|
36
|
-
cid =
|
|
37
|
-
ver = (
|
|
38
|
-
base = url ||
|
|
38
|
+
cid = @config.client_id or raise "DhanHQ.client_id not set"
|
|
39
|
+
ver = (@config.respond_to?(:ws_version) && @config.ws_version) || 2
|
|
40
|
+
base = url || @config.ws_market_feed_url
|
|
39
41
|
@url = base.include?("?") ? base : "#{base}?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
|
|
40
42
|
end
|
|
41
43
|
|
|
@@ -19,11 +19,12 @@ module DhanHQ
|
|
|
19
19
|
##
|
|
20
20
|
# Initialize Market Depth WebSocket client
|
|
21
21
|
# @param symbols [Array<String, Hash>] List of symbols (or metadata hashes) to subscribe to
|
|
22
|
+
# @param config [DhanHQ::Configuration, nil] Optional configuration instance
|
|
22
23
|
# @param options [Hash] Connection options
|
|
23
|
-
def initialize(symbols: [], **options)
|
|
24
|
-
cfg = DhanHQ.configuration
|
|
24
|
+
def initialize(symbols: [], config: nil, **options)
|
|
25
|
+
cfg = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
|
|
25
26
|
url = options[:url] || build_market_depth_url(cfg)
|
|
26
|
-
super(url: url, **options)
|
|
27
|
+
super(url: url, config: cfg, **options)
|
|
27
28
|
|
|
28
29
|
@symbols = Array(symbols)
|
|
29
30
|
@subscriptions = Concurrent::Map.new
|
|
@@ -16,15 +16,15 @@ module DhanHQ
|
|
|
16
16
|
# Maximum age of orders in tracker in seconds (default: 7 days)
|
|
17
17
|
MAX_ORDER_AGE = ENV.fetch("DHAN_WS_MAX_ORDER_AGE", 604_800).to_i
|
|
18
18
|
|
|
19
|
-
def initialize(url: nil, **options)
|
|
19
|
+
def initialize(url: nil, config: nil, **options)
|
|
20
20
|
@callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
|
|
21
21
|
@started = Concurrent::AtomicBoolean.new(false)
|
|
22
22
|
@order_tracker = Concurrent::Map.new
|
|
23
23
|
@order_timestamps = Concurrent::Map.new
|
|
24
24
|
@cleanup_mutex = Mutex.new
|
|
25
25
|
@cleanup_thread = nil
|
|
26
|
-
|
|
27
|
-
@url = url ||
|
|
26
|
+
@config = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
|
|
27
|
+
@url = url || @config.ws_order_url
|
|
28
28
|
@connection_options = options
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -35,7 +35,7 @@ module DhanHQ
|
|
|
35
35
|
return self if @started.true?
|
|
36
36
|
|
|
37
37
|
@started.make_true
|
|
38
|
-
@conn = Connection.new(url: @url, **@connection_options)
|
|
38
|
+
@conn = Connection.new(url: @url, config: @config, **@connection_options)
|
|
39
39
|
@conn.on(:open) { emit(:open, true) }
|
|
40
40
|
@conn.on(:close) { |payload| emit(:close, payload) }
|
|
41
41
|
@conn.on(:error) { |error| emit(:error, error) }
|
data/lib/dhan_hq.rb
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "logger"
|
|
5
5
|
require "zeitwerk"
|
|
6
|
-
|
|
6
|
+
begin
|
|
7
|
+
require "dotenv/load"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
# Dotenv is optional in host applications
|
|
10
|
+
end
|
|
7
11
|
require "faraday"
|
|
8
12
|
# Minimal eager requires for backward-compatible constants.
|
|
9
13
|
# These are widely referenced (e.g. `DhanHQ::BaseAPI`) and should not depend on
|
|
@@ -88,9 +92,13 @@ module DhanHQ
|
|
|
88
92
|
# @return [String]
|
|
89
93
|
BASE_URL = Constants::Urls::REST_API_BASE
|
|
90
94
|
# The current configuration instance.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
attr_reader :configuration
|
|
96
|
+
|
|
97
|
+
def configuration=(config)
|
|
98
|
+
@configuration = config
|
|
99
|
+
@default_client = nil
|
|
100
|
+
reset_model_resources!
|
|
101
|
+
end
|
|
94
102
|
|
|
95
103
|
attr_writer :logger
|
|
96
104
|
|
|
@@ -106,8 +114,10 @@ module DhanHQ
|
|
|
106
114
|
# @return [void]
|
|
107
115
|
def configure
|
|
108
116
|
self.configuration ||= Configuration.new
|
|
109
|
-
yield(configuration)
|
|
117
|
+
yield(configuration) if block_given?
|
|
110
118
|
self.logger ||= Logger.new($stdout, level: Logger::INFO)
|
|
119
|
+
@default_client = Client.new(config: configuration)
|
|
120
|
+
reset_model_resources!
|
|
111
121
|
end
|
|
112
122
|
|
|
113
123
|
# default logger so calls like DhanHQ.logger&.info never explode
|
|
@@ -118,6 +128,18 @@ module DhanHQ
|
|
|
118
128
|
@logger ||= Logger.new($stdout, level: Logger::INFO)
|
|
119
129
|
end
|
|
120
130
|
|
|
131
|
+
# Global default client (used for backward compatibility with class-level APIs)
|
|
132
|
+
#
|
|
133
|
+
# @return [DhanHQ::Client]
|
|
134
|
+
def default_client
|
|
135
|
+
@default_client ||= Client.new(config: ensure_configuration!)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Resets the global default client
|
|
139
|
+
def reset_default_client!
|
|
140
|
+
@default_client = nil
|
|
141
|
+
end
|
|
142
|
+
|
|
121
143
|
# Configures the DhanHQ client using environment variables.
|
|
122
144
|
#
|
|
123
145
|
# When credentials are injected via `DHAN_ACCESS_TOKEN` and `DHAN_CLIENT_ID` this helper
|
|
@@ -129,6 +151,8 @@ module DhanHQ
|
|
|
129
151
|
# @return [void]
|
|
130
152
|
def configure_with_env
|
|
131
153
|
self.configuration = Configuration.new
|
|
154
|
+
@default_client = Client.new(config: configuration)
|
|
155
|
+
reset_model_resources!
|
|
132
156
|
end
|
|
133
157
|
|
|
134
158
|
# Ensures configuration exists, bootstrapping from ENV when nil.
|
|
@@ -140,9 +164,32 @@ module DhanHQ
|
|
|
140
164
|
self.configuration ||= Configuration.new
|
|
141
165
|
end
|
|
142
166
|
|
|
143
|
-
# Resets the configuration to nil.
|
|
167
|
+
# Resets the configuration and default client to nil.
|
|
144
168
|
def reset_configuration!
|
|
145
169
|
self.configuration = nil
|
|
170
|
+
@default_client = nil
|
|
171
|
+
reset_model_resources!
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Clears memoized class-level resource and API instances across all models.
|
|
175
|
+
def reset_model_resources!
|
|
176
|
+
models = [
|
|
177
|
+
BaseModel,
|
|
178
|
+
Models::Order, Models::Holding, Models::Position, Models::HistoricalData,
|
|
179
|
+
Models::Trade, Models::OptionChain, Models::Funds, Models::AlertOrder,
|
|
180
|
+
Models::ForeverOrder, Models::IcebergOrder, Models::SuperOrder, Models::TwapOrder,
|
|
181
|
+
Models::LedgerEntry, Models::Instrument, Models::Edis, Models::Profile,
|
|
182
|
+
Models::Margin, Models::MultiOrder, Models::KillSwitch, Models::PnlExit,
|
|
183
|
+
Models::MarketFeed, Models::ExpiredOptionsData
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
models.each do |klass|
|
|
187
|
+
next unless klass.is_a?(Class)
|
|
188
|
+
|
|
189
|
+
klass.instance_variables.each do |var|
|
|
190
|
+
klass.instance_variable_set(var, nil) if var.to_s.end_with?("resource", "api")
|
|
191
|
+
end
|
|
192
|
+
end
|
|
146
193
|
end
|
|
147
194
|
|
|
148
195
|
# Configures the DhanHQ client by fetching credentials from a token endpoint.
|
|
@@ -164,39 +211,14 @@ module DhanHQ
|
|
|
164
211
|
def configure_from_token_endpoint(base_url: nil, bearer_token: nil)
|
|
165
212
|
base_url ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BASE_URL", nil)
|
|
166
213
|
bearer_token ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BEARER", nil)
|
|
167
|
-
|
|
168
214
|
raise DhanHQ::TokenEndpointError, "base_url and bearer_token (or ENV DHAN_TOKEN_ENDPOINT_*) are required" if base_url.to_s.empty? || bearer_token.to_s.empty?
|
|
169
215
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
c.adapter ::Faraday.default_adapter
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
response = conn.get("") do |req|
|
|
177
|
-
req.headers["Authorization"] = "Bearer #{bearer_token}"
|
|
178
|
-
req.headers["Accept"] = "application/json"
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
unless response.success?
|
|
182
|
-
body = parse_json_body(response.body)
|
|
183
|
-
msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
|
|
184
|
-
raise DhanHQ::TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
data = parse_json_body(response.body)
|
|
188
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
189
|
-
|
|
190
|
-
access_token = data["access_token"] || data[:access_token]
|
|
191
|
-
client_id = data["client_id"] || data[:client_id]
|
|
216
|
+
data = fetch_token_endpoint("#{base_url.to_s.chomp("/")}/auth/dhan/token", bearer_token, "Token endpoint")
|
|
217
|
+
access_token = data["access_token"]
|
|
218
|
+
client_id = data["client_id"]
|
|
192
219
|
raise DhanHQ::TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
|
|
193
220
|
|
|
194
|
-
|
|
195
|
-
configuration.access_token = access_token.to_s
|
|
196
|
-
configuration.client_id = client_id.to_s
|
|
197
|
-
dhan_base = data["base_url"] || data[:base_url]
|
|
198
|
-
configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
|
|
199
|
-
configuration
|
|
221
|
+
apply_token_config(access_token, client_id, data["base_url"])
|
|
200
222
|
end
|
|
201
223
|
|
|
202
224
|
# Configures the DhanHQ client by fetching credentials from a dashboard API.
|
|
@@ -208,6 +230,18 @@ module DhanHQ
|
|
|
208
230
|
def configure_from_dashboard(bearer_token:, url: "http://localhost:3011/api/dhan_access_token")
|
|
209
231
|
raise DhanHQ::TokenEndpointError, "bearer_token is required" if bearer_token.to_s.empty?
|
|
210
232
|
|
|
233
|
+
data = fetch_token_endpoint(url, bearer_token, "Dashboard")
|
|
234
|
+
access_token = data["access_token"] || data["accessToken"] || data["dhan_access_token"] || data["dhanaccesstoken"]
|
|
235
|
+
client_id = data["client_id"] || data["clientId"] || data["dhan_client_id"] || data["dhanClientId"]
|
|
236
|
+
client_id ||= configuration&.client_id || ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
237
|
+
|
|
238
|
+
raise DhanHQ::TokenEndpointError, "Dashboard response missing access_token (tried access_token, dhan_access_token, dhanaccesstoken)" if access_token.to_s.empty?
|
|
239
|
+
raise DhanHQ::TokenEndpointError, "Dashboard response missing client_id, and no fallback client_id was found in config or ENV['DHAN_CLIENT_ID']" if client_id.to_s.empty?
|
|
240
|
+
|
|
241
|
+
apply_token_config(access_token, client_id, data["base_url"] || data["baseUrl"])
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def fetch_token_endpoint(url, bearer_token, label)
|
|
211
245
|
conn = ::Faraday.new(url: url) do |c|
|
|
212
246
|
c.request :url_encoded
|
|
213
247
|
c.adapter ::Faraday.default_adapter
|
|
@@ -221,24 +255,18 @@ module DhanHQ
|
|
|
221
255
|
unless response.success?
|
|
222
256
|
body = parse_json_body(response.body)
|
|
223
257
|
msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
|
|
224
|
-
raise DhanHQ::TokenEndpointError, "
|
|
258
|
+
raise DhanHQ::TokenEndpointError, "#{label} returned #{response.status}: #{msg}"
|
|
225
259
|
end
|
|
226
260
|
|
|
227
261
|
data = parse_json_body(response.body)
|
|
228
|
-
data
|
|
229
|
-
|
|
230
|
-
access_token = data["access_token"] || data["accessToken"] || data["dhan_access_token"] || data["dhanaccesstoken"]
|
|
231
|
-
client_id = data["client_id"] || data["clientId"] || data["dhan_client_id"] || data["dhanClientId"]
|
|
232
|
-
client_id ||= self.configuration&.client_id || ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
233
|
-
|
|
234
|
-
raise DhanHQ::TokenEndpointError, "Dashboard response missing access_token (tried access_token, dhan_access_token, dhanaccesstoken)" if access_token.to_s.empty?
|
|
235
|
-
raise DhanHQ::TokenEndpointError, "Dashboard response missing client_id, and no fallback client_id was found in config or ENV['DHAN_CLIENT_ID']" if client_id.to_s.empty?
|
|
262
|
+
data.is_a?(Hash) ? data.transform_keys(&:to_s) : {}
|
|
263
|
+
end
|
|
236
264
|
|
|
265
|
+
def apply_token_config(access_token, client_id, base_url)
|
|
237
266
|
self.configuration ||= Configuration.new
|
|
238
267
|
configuration.access_token = access_token.to_s
|
|
239
268
|
configuration.client_id = client_id.to_s
|
|
240
|
-
|
|
241
|
-
configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
|
|
269
|
+
configuration.base_url = base_url.to_s if base_url.to_s != ""
|
|
242
270
|
configuration
|
|
243
271
|
end
|
|
244
272
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: DhanHQ
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shubham Taywade
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -16,98 +16,98 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
19
|
+
version: '8.0'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0'
|
|
26
|
+
version: '8.0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: base64
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0'
|
|
33
|
+
version: '0.1'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
40
|
+
version: '0.1'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: bindata
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
45
|
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
47
|
+
version: '2.4'
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
54
|
+
version: '2.4'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: concurrent-ruby
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - ">="
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
61
|
+
version: '1.3'
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
68
|
+
version: '1.3'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: csv
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
73
|
- - ">="
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0'
|
|
75
|
+
version: '3.0'
|
|
76
76
|
type: :runtime
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
80
|
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '0'
|
|
82
|
+
version: '3.0'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: dry-validation
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '1.
|
|
89
|
+
version: '1.0'
|
|
90
90
|
type: :runtime
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '1.
|
|
96
|
+
version: '1.0'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
98
|
name: eventmachine
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
100
100
|
requirements:
|
|
101
101
|
- - ">="
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '
|
|
103
|
+
version: '1.2'
|
|
104
104
|
type: :runtime
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - ">="
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '
|
|
110
|
+
version: '1.2'
|
|
111
111
|
- !ruby/object:Gem::Dependency
|
|
112
112
|
name: faraday
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -128,42 +128,42 @@ dependencies:
|
|
|
128
128
|
requirements:
|
|
129
129
|
- - ">="
|
|
130
130
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: '0'
|
|
131
|
+
version: '0.11'
|
|
132
132
|
type: :runtime
|
|
133
133
|
prerelease: false
|
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
|
136
136
|
- - ">="
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: '0'
|
|
138
|
+
version: '0.11'
|
|
139
139
|
- !ruby/object:Gem::Dependency
|
|
140
140
|
name: rotp
|
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
|
142
142
|
requirements:
|
|
143
143
|
- - ">="
|
|
144
144
|
- !ruby/object:Gem::Version
|
|
145
|
-
version: '0'
|
|
145
|
+
version: '6.0'
|
|
146
146
|
type: :runtime
|
|
147
147
|
prerelease: false
|
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
|
149
149
|
requirements:
|
|
150
150
|
- - ">="
|
|
151
151
|
- !ruby/object:Gem::Version
|
|
152
|
-
version: '0'
|
|
152
|
+
version: '6.0'
|
|
153
153
|
- !ruby/object:Gem::Dependency
|
|
154
154
|
name: zeitwerk
|
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
|
156
156
|
requirements:
|
|
157
157
|
- - ">="
|
|
158
158
|
- !ruby/object:Gem::Version
|
|
159
|
-
version: '
|
|
159
|
+
version: '2.6'
|
|
160
160
|
type: :runtime
|
|
161
161
|
prerelease: false
|
|
162
162
|
version_requirements: !ruby/object:Gem::Requirement
|
|
163
163
|
requirements:
|
|
164
164
|
- - ">="
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
|
-
version: '
|
|
166
|
+
version: '2.6'
|
|
167
167
|
description: A production-grade Ruby SDK and Ruby client for Dhan API v2 built for
|
|
168
168
|
algorithmic trading, portfolio monitoring, and live trading systems on NSE, BSE
|
|
169
169
|
and MCX. Provides typed models, token lifecycle management, dry-validation contracts,
|