ndr_lookup 0.2.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 +7 -0
- data/CHANGELOG.md +27 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +117 -0
- data/Rakefile +12 -0
- data/lib/ndr_lookup/fhir/base.rb +111 -0
- data/lib/ndr_lookup/fhir/odt/client.rb +20 -0
- data/lib/ndr_lookup/fhir/odt/organisation.rb +88 -0
- data/lib/ndr_lookup/locator_hub/client.rb +83 -0
- data/lib/ndr_lookup/locator_hub/matched_record.rb +61 -0
- data/lib/ndr_lookup/locator_hub/queue.rb +219 -0
- data/lib/ndr_lookup/nhsd_ods/client.rb +48 -0
- data/lib/ndr_lookup/nhsd_ods/code_systems.rb +31 -0
- data/lib/ndr_lookup/nhsd_ods/organisation.rb +27 -0
- data/lib/ndr_lookup/tasks.rb +1 -0
- data/lib/ndr_lookup/version.rb +3 -0
- data/lib/ndr_lookup.rb +14 -0
- data/lib/tasks/locator_hub.rake +37 -0
- metadata +204 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ef8ffa13786096c8d95f8d8454d8b1e60611d71c9d54a72f57d6d64a8c13eeda
|
4
|
+
data.tar.gz: 87ded454a35954c619f0b785af5a1eaf6b95070d7a628f60d56dd662f3de628f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d70a25899667f9006538f119a430fff5984a62aa3e97390d0e0fa99b60da33f4084ae6c2f49527d36f84b49d848568738a206d176b46bed33b1baf83aa96a4e1
|
7
|
+
data.tar.gz: 68efe8f25d0763497b39d1e1e847b523a75e5c63123c4fcf4b618a14bad0d70a834d5c1e2155d14972e4be6ce6b79fff351f81ad3a0aaba966155855c881156d
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
* no unreleased changes
|
3
|
+
|
4
|
+
## 0.1.4 / 2025-09-19
|
5
|
+
### Added
|
6
|
+
* Added NHS FHIR ODT client
|
7
|
+
|
8
|
+
## 0.1.3 / 2024-11-19
|
9
|
+
### Fixed
|
10
|
+
* Support Ruby 3.3, Rails 7.1, 7.2 and 8.0. Drop support for Ruby 3.0, Rails 6.1
|
11
|
+
|
12
|
+
### Added
|
13
|
+
* Support Ruby 3.2. Drop support for Ruby 2.7, Rails 6.0
|
14
|
+
|
15
|
+
## 0.1.2 / 2022-12-08
|
16
|
+
### Fixed
|
17
|
+
* Drop support for Rails 5.2
|
18
|
+
* Support Ruby 3.1, Rails 7.0
|
19
|
+
|
20
|
+
## 0.1.1 / 2022-11-15
|
21
|
+
### Added
|
22
|
+
* Added NHS Digital ODS API client
|
23
|
+
* Drop support for Ruby 2.6
|
24
|
+
|
25
|
+
## 0.1.0 / Unreleased
|
26
|
+
### Added
|
27
|
+
* Added existing ArcGIS LocatorHub address `Rectify` client
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at timgentry@users.noreply.github.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 timgentry
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# NdrLookup [](https://github.com/NHSDigital/ndr_lookup/actions?query=workflow%3Atest)
|
2
|
+
|
3
|
+
This is the NHS Digital (NHSD) National Disease Registers (NDR) Lookup ruby gem,
|
4
|
+
providing:
|
5
|
+
|
6
|
+
1. an ArcGIS LocatorHub API client
|
7
|
+
2. NHS Digital Organisation Data Service API Client
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'ndr_lookup'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install ndr_lookup
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### ArcGIS LocatorHub API Client
|
28
|
+
|
29
|
+
`NdrLookup::LocatorHub::Client` is a client to access the LocatorHub API to rectify given addresses.
|
30
|
+
|
31
|
+
The client isn't included by default, so add the following to your code:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require 'ndr_lookup/locator_hub/client'
|
35
|
+
```
|
36
|
+
|
37
|
+
It requires some manual setup:
|
38
|
+
|
39
|
+
Configuration | Description
|
40
|
+
--- | ---
|
41
|
+
`domain` | A string of the authenicating domain name.
|
42
|
+
`username` | A string of the authenticating username.
|
43
|
+
`password` | A string of the authenticating password.
|
44
|
+
|
45
|
+
Within a Rails application, you should do this in an initializer, preferably using encrypted credentials, e.g.:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# This file configures NdrLookup.
|
49
|
+
Rails.application.config.to_prepare do
|
50
|
+
NdrLookup::LocatorHub::Client.domain = Rails.application.credentials.production[:locator_hub_api][:domain]
|
51
|
+
NdrLookup::LocatorHub::Client.username = Rails.application.credentials.production[:locator_hub_api][:username]
|
52
|
+
NdrLookup::LocatorHub::Client.password = Rails.application.credentials.production[:locator_hub_api][:password]
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
### NHS Digital Organisation Data Service API Client
|
57
|
+
|
58
|
+
NdrLookup::NhsdOds::Client is a client to access the NHS Digital ODS API search and sync endpoints.
|
59
|
+
|
60
|
+
Sync will return a JSON format payload for all organisations that have been updated since the specified date:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
require 'ndr_lookup/nhsd_ods/client'
|
64
|
+
|
65
|
+
organisation_ids = NdrLookup::NhsdOds::Client.sync(Date.new(2019, 6, 14))
|
66
|
+
```
|
67
|
+
|
68
|
+
*NOTE* that the client isn't included by default, so you have to require it specifically.
|
69
|
+
|
70
|
+
Search will allow you to search based on parameters specified by ODS
|
71
|
+
https://digital.nhs.uk/services/organisation-data-service/guidance-for-developers/search-endpoint#parameters
|
72
|
+
|
73
|
+
For example:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
require 'ndr_lookup/nhsd_ods/client'
|
77
|
+
|
78
|
+
results = NdrLookup::NhsdOds::Client.search(Name: 'NHS Digital')
|
79
|
+
```
|
80
|
+
|
81
|
+
NdrLookup::NhsdOds::Organisation will enable you to access the NHS Digital ODS API to find a specific organisation.
|
82
|
+
|
83
|
+
Lookup will return an Organisation object with model like behaviours:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
require 'ndr_lookup/nhsd_ods/organisation'
|
87
|
+
|
88
|
+
organisation = NdrLookup::NhsdOds::Organisation.find('X26')
|
89
|
+
```
|
90
|
+
|
91
|
+
NdrLookup::NhsdOds::CodeSystems will enable you to access the NHS Digital ODS API to find the meta data information included in the payload records returned via the endpoints. CodeSystems currently includes roles, relationships and record classes.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
require 'ndr_lookup/nhsd_ods/code_systems'
|
95
|
+
|
96
|
+
role = NdrLookup::NhsdOds::Role.find('RO197')
|
97
|
+
first_relationship = NdrLookup::NhsdOds::Rel.first
|
98
|
+
all_record_classes = NdrLookup::NhsdOds::Recordclass.all
|
99
|
+
```
|
100
|
+
|
101
|
+
## Development
|
102
|
+
|
103
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
104
|
+
|
105
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
106
|
+
|
107
|
+
## Contributing
|
108
|
+
|
109
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/NHSDigital/ndr_lookup. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
110
|
+
|
111
|
+
## License
|
112
|
+
|
113
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
114
|
+
|
115
|
+
## Code of Conduct
|
116
|
+
|
117
|
+
Everyone interacting in the NdrLookup project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/NHSDigital/ndr_lookup/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'ndr_dev_support/tasks'
|
4
|
+
require 'ndr_lookup/tasks'
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << 'test'
|
8
|
+
t.libs << 'lib'
|
9
|
+
t.test_files = FileList['test/**/*_test.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
task default: :test
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'active_resource'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
|
5
|
+
module NdrLookup
|
6
|
+
module Fhir
|
7
|
+
# Client for interacting with the NHS Digital FHIR API
|
8
|
+
class Base < ActiveResource::Base
|
9
|
+
class ApiError < StandardError; end
|
10
|
+
class ResourceNotFound < ApiError; end
|
11
|
+
class InvalidResponse < ApiError; end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_writer :additional_headers
|
15
|
+
|
16
|
+
def additional_headers
|
17
|
+
@additional_headers ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def sync
|
21
|
+
raise 'Must be defined in subclasses!'
|
22
|
+
end
|
23
|
+
|
24
|
+
# Finds a specific FHIR resource by type and ID
|
25
|
+
# @return [Hash] Parsed FHIR resource
|
26
|
+
def find(resource_type, id)
|
27
|
+
response = connection.get(
|
28
|
+
"#{endpoint}/#{resource_type}/#{id}",
|
29
|
+
headers
|
30
|
+
)
|
31
|
+
JSON.parse(response.body)
|
32
|
+
rescue ActiveResource::ResourceNotFound
|
33
|
+
raise ResourceNotFound, "#{resource_type} with ID '#{id}' not found"
|
34
|
+
rescue JSON::ParserError
|
35
|
+
raise InvalidResponse, 'Invalid JSON response from server'
|
36
|
+
rescue StandardError => e
|
37
|
+
raise ApiError, "Unexpected error: #{e.message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def endpoint
|
41
|
+
raise 'Must be defined in subclasses!'
|
42
|
+
end
|
43
|
+
|
44
|
+
# Searches for FHIR resources using the provided parameters
|
45
|
+
# @param resource_type [String] The type of FHIR resource to search for
|
46
|
+
# (e.g. 'Organization', 'OrganizationAffiliation')
|
47
|
+
# @param params [Hash] Search parameters specific to the resource type
|
48
|
+
# @return [Hash] FHIR Bundle containing search results
|
49
|
+
# @example Search for organizations
|
50
|
+
# Client.search('Organization', lastUpdated: 'gt2024-01-01')
|
51
|
+
# @example Search for relationships
|
52
|
+
# Client.search('OrganizationAffiliation', organization: 'RHAGX')
|
53
|
+
def search(resource_type, params = {})
|
54
|
+
url = construct_url(endpoint, resource_type, params)
|
55
|
+
|
56
|
+
# Make the request
|
57
|
+
response = connection.get(url, headers)
|
58
|
+
|
59
|
+
# Process response
|
60
|
+
payload = JSON.parse(response.body)
|
61
|
+
raise_unless_response_success(response, payload)
|
62
|
+
|
63
|
+
payload
|
64
|
+
rescue JSON::ParserError
|
65
|
+
raise InvalidResponse, 'Invalid JSON response from server'
|
66
|
+
rescue StandardError => e
|
67
|
+
raise ApiError, "Search failed: #{e.message}"
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def construct_url(endpoint, resource_type, params = {})
|
73
|
+
url = "#{endpoint}/#{resource_type}"
|
74
|
+
|
75
|
+
# Add query parameters if any exist
|
76
|
+
if params.any?
|
77
|
+
# Convert params to proper query string format
|
78
|
+
query_string = params.map do |key, value|
|
79
|
+
"#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
|
80
|
+
end.join('&')
|
81
|
+
|
82
|
+
url = "#{url}?#{query_string}"
|
83
|
+
end
|
84
|
+
|
85
|
+
url
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [ActiveResource::Connection] Configured connection instance
|
89
|
+
def connection
|
90
|
+
@connection ||= ActiveResource::Connection.new(endpoint)
|
91
|
+
end
|
92
|
+
|
93
|
+
# @return [Hash] FHIR API required headers
|
94
|
+
def headers
|
95
|
+
{
|
96
|
+
'Accept' => 'application/fhir+json',
|
97
|
+
'Content-Type' => 'application/fhir+json'
|
98
|
+
}.merge(additional_headers)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Raises an error unless response is successful
|
102
|
+
def raise_unless_response_success(response, payload)
|
103
|
+
return if response.code == '200'
|
104
|
+
|
105
|
+
error_message = payload['errorText'] || 'Unknown error'
|
106
|
+
raise ApiError, "#{payload['errorCode']} - #{error_message}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative '../base'
|
2
|
+
|
3
|
+
module NdrLookup
|
4
|
+
module Fhir
|
5
|
+
module Odt
|
6
|
+
# Client for interacting with the NHS Digital FHIR API
|
7
|
+
# See https://digital.nhs.uk/developer/api-catalogue/organisation-data-terminology
|
8
|
+
class Client < Base
|
9
|
+
FHIR_ODT_ENDPOINT = 'https://api.service.nhs.uk/organisation-data-terminology-api/fhir'.freeze
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Wrapped in method to enable stubbing in tests (constants are hard to stub)
|
13
|
+
def endpoint
|
14
|
+
FHIR_ODT_ENDPOINT
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_resource'
|
3
|
+
require_relative 'client'
|
4
|
+
|
5
|
+
module NdrLookup
|
6
|
+
module Fhir
|
7
|
+
module Odt
|
8
|
+
# Represents an Organization resource from the NHS Digital FHIR API
|
9
|
+
# The main endpoint path we are using is 'OrganiSation' but full API path calls also include 'OrganiZation'
|
10
|
+
class Organisation < Base
|
11
|
+
self.include_format_in_path = false
|
12
|
+
self.site = Client::FHIR_ODT_ENDPOINT
|
13
|
+
self.collection_name = 'Organization'
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Finds a specific organization by ID
|
17
|
+
# @return [Organisation] The found organization
|
18
|
+
def find(id)
|
19
|
+
response = Client.find('Organization', id)
|
20
|
+
new(response)
|
21
|
+
rescue Client::ResourceNotFound => e
|
22
|
+
logger.info("Organization not found: #{e.message}")
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# ActiveRecord subs .all in to /all which then becomes like finding the id 'all'
|
27
|
+
# so we have to explicitly block this if we want it to not be a thing.
|
28
|
+
def all
|
29
|
+
raise NotImplementedError, 'Use search method instead of all'
|
30
|
+
end
|
31
|
+
|
32
|
+
# Searches for organizations matching the provided parameters
|
33
|
+
# @return [Array<Organisation>] Matching organizations
|
34
|
+
def search(params = {})
|
35
|
+
response = Client.search('Organization', params)
|
36
|
+
return [] unless response['entry']
|
37
|
+
|
38
|
+
response['entry'].map { |entry| new(entry['resource']) }
|
39
|
+
rescue Client::ApiError => e
|
40
|
+
logger.info("Organization search failed: #{e.message}")
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
44
|
+
def sync(date)
|
45
|
+
formatted_date = case date
|
46
|
+
when Date, Time
|
47
|
+
date.strftime('%Y-%m-%d')
|
48
|
+
else
|
49
|
+
date.to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
search_params = { _lastUpdated: "gt#{formatted_date}" }
|
53
|
+
search(search_params)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def logger
|
59
|
+
@logger ||= if defined?(Rails) && Rails.respond_to?(:logger)
|
60
|
+
Rails.logger
|
61
|
+
else
|
62
|
+
Logger.new($stdout)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Initializes a new Organization with downcased attributes
|
68
|
+
def initialize(attributes = {})
|
69
|
+
attributes.deep_transform_keys!(&:downcase) if attributes.is_a?(Hash)
|
70
|
+
super
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Prevents Date constants from raising errors
|
76
|
+
# When ActiveResource tries to process something called 'Date'
|
77
|
+
# it would call const_valid?('Date', false).
|
78
|
+
# This would return false, making ActiveResource skip trying to create a Date constant and
|
79
|
+
# instead fall back to creating an :UnnamedResource.
|
80
|
+
def const_valid?(*const_args)
|
81
|
+
return false if const_args.first == 'Date'
|
82
|
+
|
83
|
+
super
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'active_support/core_ext/module'
|
2
|
+
require 'net/http'
|
3
|
+
require 'rubyntlm'
|
4
|
+
require 'httpi'
|
5
|
+
require 'json'
|
6
|
+
require_relative 'matched_record'
|
7
|
+
|
8
|
+
module NdrLookup
|
9
|
+
module LocatorHub
|
10
|
+
# This class is the LocatorHub API client
|
11
|
+
class Client
|
12
|
+
LIST_LOCATORS_PATH = 'listlocators?format=json'.freeze
|
13
|
+
|
14
|
+
LOCATOR_ID = 'LocatorId'.freeze
|
15
|
+
LOCATOR_NAME = 'LocatorName'.freeze
|
16
|
+
|
17
|
+
ADDRESS_BASE_PREMIUM_WA = 'AddressBasePremiumWA'.freeze
|
18
|
+
|
19
|
+
# The authenticating domain credentials of the API,
|
20
|
+
# all of which must be configured by the host app
|
21
|
+
cattr_accessor :domain, :password, :username
|
22
|
+
self.domain = nil
|
23
|
+
self.password = nil
|
24
|
+
self.username = nil
|
25
|
+
|
26
|
+
def initialize(api_path)
|
27
|
+
check_credentials
|
28
|
+
@api_path = api_path
|
29
|
+
end
|
30
|
+
|
31
|
+
def locator_id
|
32
|
+
@locator_id ||= locator[LOCATOR_ID]
|
33
|
+
end
|
34
|
+
|
35
|
+
def rectify_address(address)
|
36
|
+
query = URI.encode_www_form(
|
37
|
+
Query: address,
|
38
|
+
Fuzzy: 'false',
|
39
|
+
ReturnCoordinateSystem: '-1',
|
40
|
+
format: 'json'
|
41
|
+
)
|
42
|
+
response = call_postcode_service("Rectify/#{locator_id}/ADDRESS?#{query}")
|
43
|
+
MatchedRecord.new(response)
|
44
|
+
end
|
45
|
+
|
46
|
+
# def capabilities
|
47
|
+
# call_postcode_service("Capabilities/#{locator_id}?format=json")
|
48
|
+
# end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def listlocators
|
53
|
+
call_postcode_service(LIST_LOCATORS_PATH)
|
54
|
+
end
|
55
|
+
|
56
|
+
def locator
|
57
|
+
listlocators.find { |l| l[LOCATOR_NAME] == ADDRESS_BASE_PREMIUM_WA }
|
58
|
+
end
|
59
|
+
|
60
|
+
def request
|
61
|
+
@request ||= begin
|
62
|
+
request = HTTPI::Request.new
|
63
|
+
request.auth.ntlm(username, password, domain)
|
64
|
+
request
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def call_postcode_service(path)
|
69
|
+
request.url = [@api_path, path].join('/')
|
70
|
+
response = HTTPI.get(request)
|
71
|
+
JSON.parse(response.body)
|
72
|
+
end
|
73
|
+
|
74
|
+
def check_credentials
|
75
|
+
unset_attributes = %i[domain password username].select { |attr| send(attr).nil? }
|
76
|
+
return if unset_attributes.empty?
|
77
|
+
|
78
|
+
raise "NdrLookup::LocatorHub::Client attributes #{unset_attributes.join(', ')} " \
|
79
|
+
'have not been configured'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module NdrLookup
|
2
|
+
module LocatorHub
|
3
|
+
# This class wraps the client response, creating a hash of matched record
|
4
|
+
# columns and provides custom getter methods for cleaning/casting specific columns
|
5
|
+
class MatchedRecord
|
6
|
+
RECTIFY_RECORD_SCORE = 'RectifyRecordScore'.freeze
|
7
|
+
MATCHED_RECORD = 'MatchedRecord'.freeze
|
8
|
+
COLUMNS = 'Columns'.freeze
|
9
|
+
N = 'N'.freeze
|
10
|
+
R = 'R'.freeze
|
11
|
+
|
12
|
+
LOCATOR_DESCRIPTION = 'LOCATOR_DESCRIPTION'.freeze
|
13
|
+
ADMINISTRATIVE_AREA = 'ADMINISTRATIVE_AREA'.freeze
|
14
|
+
POSTCODE = 'POSTCODE'.freeze
|
15
|
+
UDPRN = 'DPA_UDPRN'.freeze
|
16
|
+
|
17
|
+
attr_reader :score
|
18
|
+
|
19
|
+
def initialize(response)
|
20
|
+
@score = response[RECTIFY_RECORD_SCORE]
|
21
|
+
@record = response[MATCHED_RECORD]
|
22
|
+
end
|
23
|
+
|
24
|
+
def locator_description
|
25
|
+
@locator_description ||= columns[LOCATOR_DESCRIPTION].
|
26
|
+
gsub(/\|LOCATOR_SEPARATOR\|/, ', ')
|
27
|
+
end
|
28
|
+
|
29
|
+
def administrative_area
|
30
|
+
@administrative_area ||= columns[ADMINISTRATIVE_AREA]
|
31
|
+
end
|
32
|
+
|
33
|
+
def locator_description_with_administrative_area
|
34
|
+
return locator_description if locator_description.include?(administrative_area)
|
35
|
+
|
36
|
+
parts = locator_description.split(', ')
|
37
|
+
(parts[0..-2] << administrative_area << parts[-1]).join(', ')
|
38
|
+
end
|
39
|
+
|
40
|
+
def postcode
|
41
|
+
@postcode ||= columns[POSTCODE]
|
42
|
+
end
|
43
|
+
|
44
|
+
def udprn
|
45
|
+
@udprn ||= columns[UDPRN] == columns[UDPRN].to_i.to_s ? columns[UDPRN].to_i : columns[UDPRN]
|
46
|
+
end
|
47
|
+
|
48
|
+
# keys are returned in response['MatchedRecord']['Columns']['N'] and values are returned
|
49
|
+
# in response['MatchedRecord']['Columns']['R'] and we turn them back into a normal hash
|
50
|
+
def columns
|
51
|
+
@columns ||= begin
|
52
|
+
columns = {}
|
53
|
+
@record && @record[COLUMNS].each.with_index do |column, i|
|
54
|
+
columns[column[N]] = @record[R][i]
|
55
|
+
end
|
56
|
+
columns
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require_relative 'client'
|
2
|
+
|
3
|
+
module NdrLookup
|
4
|
+
module LocatorHub
|
5
|
+
# Thread queue
|
6
|
+
class Queue
|
7
|
+
BATCH_SIZE = 100
|
8
|
+
|
9
|
+
def initialize(api_path, klass, max_id, total_count)
|
10
|
+
@api_path = api_path
|
11
|
+
@klass = klass
|
12
|
+
@max_id = max_id
|
13
|
+
@total_count = total_count
|
14
|
+
@max_worker_count = 8
|
15
|
+
@worker_count = 1
|
16
|
+
|
17
|
+
queue_records
|
18
|
+
|
19
|
+
# @threads = [record_getter, max_rate_limiter, rate_limiter, record_setter, monitor]
|
20
|
+
@threads = [record_getter, record_setter]
|
21
|
+
|
22
|
+
t0 = Time.current
|
23
|
+
@max_worker_count.times { |i| @threads << worker(i) }
|
24
|
+
|
25
|
+
@threads.map(&:join)
|
26
|
+
Rails.logger.info "Total Time taken: #{Time.current - t0} secs"
|
27
|
+
Rails.logger.info "Time taken (per address): #{(Time.current - t0) / BATCH_SIZE} secs"
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Pretty form of address field.
|
33
|
+
def print_full_address(object)
|
34
|
+
"#{object.address&.gsub(/,/, ', ')&.titleize}, " \
|
35
|
+
"#{object.postcode}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def input_queue
|
39
|
+
@input_queue ||= ::Queue.new
|
40
|
+
end
|
41
|
+
|
42
|
+
def output_queue
|
43
|
+
@output_queue ||= ::Queue.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def worker(i)
|
47
|
+
looping_thread("worker #{i}", 120) do
|
48
|
+
connect_client do |locator_hub|
|
49
|
+
while (i + 1) > @worker_count
|
50
|
+
sleep 10
|
51
|
+
raise ThreadError if @worker_count.zero?
|
52
|
+
end
|
53
|
+
raise ThreadError if @worker_count.zero?
|
54
|
+
|
55
|
+
rec = input_queue.pop(true)
|
56
|
+
next if rec.address.blank? && rec.postcode.blank?
|
57
|
+
result = locator_hub.rectify_address(print_full_address(rec))
|
58
|
+
next unless result.postcode == rec.postcode
|
59
|
+
|
60
|
+
lcs_percent = lcs_commonality_percentage(print_full_address(rec),
|
61
|
+
result.locator_description)
|
62
|
+
|
63
|
+
if result.score >= 85 || (result.score >= 70 && lcs_percent >= 80)
|
64
|
+
rec.udprn = result.udprn if result.udprn.present?
|
65
|
+
next unless rec.changed? && rec.valid?
|
66
|
+
|
67
|
+
output_queue.push(rec)
|
68
|
+
else
|
69
|
+
# rubocop:disable Rails/Output
|
70
|
+
puts
|
71
|
+
puts "locator_description: #{result.locator_description.inspect}"
|
72
|
+
debug_scores(rec, result, lcs_percent)
|
73
|
+
|
74
|
+
lcs_percent2 = lcs_commonality_percentage(
|
75
|
+
print_full_address(rec),
|
76
|
+
result.locator_description_with_administrative_area
|
77
|
+
)
|
78
|
+
if lcs_percent2 != lcs_percent
|
79
|
+
puts 'locator_description_with_administrative_area: ' +
|
80
|
+
result.locator_description_with_administrative_area.inspect
|
81
|
+
debug_scores(rec, result, lcs_percent2)
|
82
|
+
# rubocop:enable Rails/Output
|
83
|
+
end
|
84
|
+
# require 'pry'; binding.pry
|
85
|
+
end
|
86
|
+
end # connect_client
|
87
|
+
|
88
|
+
Rails.logger.info 'Sleeping...'
|
89
|
+
end # looping_thread
|
90
|
+
end
|
91
|
+
|
92
|
+
def debug_scores(rec, result, lcs_percent)
|
93
|
+
# rubocop:disable Rails/Output
|
94
|
+
puts rec.id
|
95
|
+
puts print_full_address(rec)
|
96
|
+
puts "Match Score: #{result.score}"
|
97
|
+
puts "LCS: #{lcs_percent}%"
|
98
|
+
# rubocop:enable Rails/Output
|
99
|
+
end
|
100
|
+
|
101
|
+
# This method connects to LocatorHub, yields the connection and handles exceptions.
|
102
|
+
# It will supress everything except database and thread exceptions.
|
103
|
+
def connect_client
|
104
|
+
raise(ArgumentError, 'block required') unless block_given?
|
105
|
+
|
106
|
+
client = LocatorHub::Client.new(@api_path)
|
107
|
+
loop do
|
108
|
+
yield(client)
|
109
|
+
end
|
110
|
+
rescue OCIError, ThreadError => e
|
111
|
+
raise e
|
112
|
+
rescue StandardError => e
|
113
|
+
Rails.logger.info e.inspect
|
114
|
+
end
|
115
|
+
|
116
|
+
def looping_thread(name, sleep_duration)
|
117
|
+
Thread.new do
|
118
|
+
loop do
|
119
|
+
raise ThreadError if @worker_count.zero?
|
120
|
+
|
121
|
+
yield
|
122
|
+
|
123
|
+
sleep sleep_duration
|
124
|
+
end
|
125
|
+
rescue ThreadError
|
126
|
+
Rails.logger.info "#{name} thread finished!"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def monitor
|
131
|
+
@monitor ||= looping_thread('monitor', 1) do
|
132
|
+
Rails.logger.info "input_queue: #{input_queue.length} " \
|
133
|
+
"output_queue: #{output_queue.length}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def record_getter
|
138
|
+
@record_getter ||= looping_thread('record_getter', 1) do
|
139
|
+
queue_records if input_queue.length < 25
|
140
|
+
|
141
|
+
raise ThreadError if input_queue.empty?
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def record_setter
|
146
|
+
@record_setter ||= looping_thread('record_setter', 1) do
|
147
|
+
persist_records if output_queue.length > BATCH_SIZE
|
148
|
+
|
149
|
+
raise ThreadError if input_queue.empty? && output_queue.empty?
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def weekday?
|
154
|
+
(1..5).cover?(Time.current.wday)
|
155
|
+
end
|
156
|
+
|
157
|
+
def max_rate_limiter
|
158
|
+
@max_rate_limiter ||= looping_thread('max_rate_limiter', 10) do
|
159
|
+
@max_worker_count = ask('Max Threads: ').to_i
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def rate_limiter
|
164
|
+
@rate_limiter ||= looping_thread('rate_limiter', 60) do
|
165
|
+
@worker_count = weekday? && (8..15).cover?(Time.current.hour) ? 2 : 10
|
166
|
+
@worker_count = @max_worker_count if @max_worker_count < @worker_count
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def lcs_commonality_percentage(a, b)
|
171
|
+
squashed_a = a.upcase.gsub(/[,\.\s]+/, '')
|
172
|
+
squashed_b = b.upcase.gsub(/[,\.\s]+/, '')
|
173
|
+
shorter_length = [squashed_a, squashed_b].map(&:length).min
|
174
|
+
|
175
|
+
lcs = Diff::LCS.lcs(squashed_a, squashed_b)
|
176
|
+
100.0 * lcs.length / shorter_length
|
177
|
+
end
|
178
|
+
|
179
|
+
def queue_records
|
180
|
+
# Rails.logger.info "pushing #{BATCH_SIZE} records onto the queue"
|
181
|
+
records = get_records_below_id(@max_id, BATCH_SIZE)
|
182
|
+
records.each { |record| input_queue.push(record) }
|
183
|
+
end
|
184
|
+
|
185
|
+
def get_records_below_id(id, limit = BATCH_SIZE)
|
186
|
+
# find_in_batches ignores order
|
187
|
+
# records = Address.includes(:patient).where(udprn: nil).
|
188
|
+
records = @klass.where(udprn: nil).
|
189
|
+
where("#{@klass.primary_key} < ?", id).
|
190
|
+
order("#{@klass.primary_key} desc").
|
191
|
+
limit(limit)
|
192
|
+
@max_id = records.last.id
|
193
|
+
records
|
194
|
+
end
|
195
|
+
|
196
|
+
def persist_records
|
197
|
+
Rails.logger.info 'persist_records'
|
198
|
+
@klass.transaction do
|
199
|
+
change_count = 0
|
200
|
+
|
201
|
+
output_queue.length.times do
|
202
|
+
rec = output_queue.pop(true)
|
203
|
+
|
204
|
+
# # next unless rec.changed? && rec.valid?
|
205
|
+
# # Rails.logger.info "CHANGES: #{rec.changes.inspect}"
|
206
|
+
next unless rec.save
|
207
|
+
change_count += 1
|
208
|
+
@total_count += 1
|
209
|
+
end
|
210
|
+
# rubocop:disable Rails/Output
|
211
|
+
puts "Change count: #{change_count} (Total: #{@total_count})" \
|
212
|
+
" Last id: #{@max_id}"
|
213
|
+
puts "worker_count: #{@worker_count} max_worker_count: #{@max_worker_count}"
|
214
|
+
# rubocop:enable Rails/Output
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_resource'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
require 'httpi'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module NdrLookup
|
8
|
+
module NhsdOds
|
9
|
+
# The API Client for hitting the sync and search endpoints
|
10
|
+
class Client
|
11
|
+
ENDPOINT = 'https://directory.spineservices.nhs.uk/ORD/2-0-0/'.freeze
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def sync(date)
|
15
|
+
date = date.to_date
|
16
|
+
raise ArgumentError, 'invalid date' unless date.is_a?(Date)
|
17
|
+
|
18
|
+
request = ENDPOINT + 'sync?LastChangeDate=' + date.strftime('%F')
|
19
|
+
response = HTTPI.get(request)
|
20
|
+
payload = JSON.parse(response.body)
|
21
|
+
|
22
|
+
raise_unless_response_success(response, payload)
|
23
|
+
|
24
|
+
payload['Organisations']
|
25
|
+
end
|
26
|
+
|
27
|
+
def search(params = {})
|
28
|
+
# Expecting the keys to be valid query parameters
|
29
|
+
# https://digital.nhs.uk/services/organisation-data-service/guidance-for-developers/search-endpoint#parameters
|
30
|
+
query = HTTPI::QueryBuilder::Flat.build(params)
|
31
|
+
request = ENDPOINT + 'organisations?' + query
|
32
|
+
response = HTTPI.get(request)
|
33
|
+
payload = JSON.parse(response.body)
|
34
|
+
|
35
|
+
raise_unless_response_success(response, payload)
|
36
|
+
|
37
|
+
payload['Organisations']
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def raise_unless_response_success(response, payload)
|
43
|
+
raise "#{payload['errorCode']} - #{payload['errorText']}" unless response.code == 200
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_resource'
|
3
|
+
require_relative 'client'
|
4
|
+
|
5
|
+
module NdrLookup
|
6
|
+
module NhsdOds
|
7
|
+
# Retrieve CodeSystems information for NHS Digital ODS meta data.
|
8
|
+
class CodeSystems < ActiveResource::Base
|
9
|
+
self.include_format_in_path = false
|
10
|
+
self.site = Client::ENDPOINT
|
11
|
+
|
12
|
+
def initialize(attributes, persisted = false)
|
13
|
+
attributes = attributes.first if attributes.is_a? Array
|
14
|
+
attributes.deep_transform_keys!(&:downcase)
|
15
|
+
super(attributes, persisted)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Retrieve CodeSystems information for Record class meta data.
|
20
|
+
class Recordclass < CodeSystems
|
21
|
+
end
|
22
|
+
|
23
|
+
# Retrieve CodeSystems information for Relationships meta data.
|
24
|
+
class Rel < CodeSystems
|
25
|
+
end
|
26
|
+
|
27
|
+
# Retrieve CodeSystems information for Roles meta data.
|
28
|
+
class Role < CodeSystems
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_resource'
|
3
|
+
require_relative 'client'
|
4
|
+
|
5
|
+
module NdrLookup
|
6
|
+
module NhsdOds
|
7
|
+
# The orgaisation class to allow organisation to work like you would expect a model to work
|
8
|
+
# The API only supports .find <code>
|
9
|
+
class Organisation < ActiveResource::Base
|
10
|
+
self.include_format_in_path = false
|
11
|
+
self.site = Client::ENDPOINT
|
12
|
+
|
13
|
+
def initialize(attributes, persisted = false)
|
14
|
+
attributes.deep_transform_keys!(&:downcase)
|
15
|
+
super(attributes, persisted)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def const_valid?(*const_args)
|
21
|
+
return false if const_args.first == 'Date'
|
22
|
+
|
23
|
+
super(*const_args)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
load 'tasks/locator_hub.rake'
|
data/lib/ndr_lookup.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'ndr_lookup/locator_hub/matched_record'
|
2
|
+
require 'ndr_lookup/locator_hub/client'
|
3
|
+
require 'ndr_lookup/locator_hub/queue'
|
4
|
+
require 'ndr_lookup/nhsd_ods/code_systems'
|
5
|
+
require 'ndr_lookup/nhsd_ods/client'
|
6
|
+
require 'ndr_lookup/nhsd_ods/organisation'
|
7
|
+
require 'ndr_lookup/version'
|
8
|
+
require 'ndr_lookup/fhir/odt/client'
|
9
|
+
require 'ndr_lookup/fhir/odt/organisation'
|
10
|
+
|
11
|
+
module NdrLookup
|
12
|
+
class Error < StandardError; end
|
13
|
+
# Your code goes here...
|
14
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
namespace :locator_hub do
|
2
|
+
desc 'setup'
|
3
|
+
task :setup do
|
4
|
+
require 'highline/import'
|
5
|
+
require 'ndr_lookup/locator_hub/client'
|
6
|
+
|
7
|
+
NdrLookup::LocatorHub::Client.domain = ask('Domain name: ')
|
8
|
+
NdrLookup::LocatorHub::Client.username = ask('Domain Username: ')
|
9
|
+
NdrLookup::LocatorHub::Client.password = ask('Domain Password: ') { |q| q.echo = false }
|
10
|
+
|
11
|
+
@api_path = ask('API Path: ')
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'locator'
|
15
|
+
task locator: :setup do
|
16
|
+
HTTPI.log = false
|
17
|
+
# HTTPI.log_level = :warn
|
18
|
+
|
19
|
+
klass = Address
|
20
|
+
max_id = klass.where('udprn is not null').maximum(:id)
|
21
|
+
puts max_id
|
22
|
+
total_count = klass.where('udprn is not null').count
|
23
|
+
|
24
|
+
LocatorHub::Queue.new(@api_path, klass, max_id, total_count)
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'rectify_address'
|
28
|
+
task rectify_address: :setup do
|
29
|
+
# Usage: bundle exec rake locator_hub:rectify_address
|
30
|
+
|
31
|
+
address = ask('Address: ')
|
32
|
+
|
33
|
+
client = NdrLookup::LocatorHub::Client.new(@api_path)
|
34
|
+
matched_record = client.rectify_address(address)
|
35
|
+
puts matched_record.inspect
|
36
|
+
end
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ndr_lookup
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- NCRS Development Team
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-09-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activeresource
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '7'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '6.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '7'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activesupport
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '7.0'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '8.1'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '7.0'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '8.1'
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: httpi
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '4.0'
|
60
|
+
type: :runtime
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '4.0'
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: rubyntlm
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0.6'
|
74
|
+
type: :runtime
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0.6'
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: bundler
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '2.0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '2.0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: minitest
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '5.0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - "~>"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '5.0'
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: ndr_dev_support
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '6.0'
|
116
|
+
- - "<"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '8.0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '6.0'
|
126
|
+
- - "<"
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '8.0'
|
129
|
+
- !ruby/object:Gem::Dependency
|
130
|
+
name: rake
|
131
|
+
requirement: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: 12.3.3
|
136
|
+
type: :development
|
137
|
+
prerelease: false
|
138
|
+
version_requirements: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: 12.3.3
|
143
|
+
- !ruby/object:Gem::Dependency
|
144
|
+
name: webmock
|
145
|
+
requirement: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: '0'
|
157
|
+
description: NDR library to consume lookup data APIs
|
158
|
+
email: []
|
159
|
+
executables: []
|
160
|
+
extensions: []
|
161
|
+
extra_rdoc_files: []
|
162
|
+
files:
|
163
|
+
- CHANGELOG.md
|
164
|
+
- CODE_OF_CONDUCT.md
|
165
|
+
- LICENSE.txt
|
166
|
+
- README.md
|
167
|
+
- Rakefile
|
168
|
+
- lib/ndr_lookup.rb
|
169
|
+
- lib/ndr_lookup/fhir/base.rb
|
170
|
+
- lib/ndr_lookup/fhir/odt/client.rb
|
171
|
+
- lib/ndr_lookup/fhir/odt/organisation.rb
|
172
|
+
- lib/ndr_lookup/locator_hub/client.rb
|
173
|
+
- lib/ndr_lookup/locator_hub/matched_record.rb
|
174
|
+
- lib/ndr_lookup/locator_hub/queue.rb
|
175
|
+
- lib/ndr_lookup/nhsd_ods/client.rb
|
176
|
+
- lib/ndr_lookup/nhsd_ods/code_systems.rb
|
177
|
+
- lib/ndr_lookup/nhsd_ods/organisation.rb
|
178
|
+
- lib/ndr_lookup/tasks.rb
|
179
|
+
- lib/ndr_lookup/version.rb
|
180
|
+
- lib/tasks/locator_hub.rake
|
181
|
+
homepage: https://github.com/NHSDigital/ndr_lookup
|
182
|
+
licenses:
|
183
|
+
- MIT
|
184
|
+
metadata: {}
|
185
|
+
post_install_message:
|
186
|
+
rdoc_options: []
|
187
|
+
require_paths:
|
188
|
+
- lib
|
189
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - ">="
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '3.0'
|
194
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
195
|
+
requirements:
|
196
|
+
- - ">="
|
197
|
+
- !ruby/object:Gem::Version
|
198
|
+
version: '0'
|
199
|
+
requirements: []
|
200
|
+
rubygems_version: 3.4.19
|
201
|
+
signing_key:
|
202
|
+
specification_version: 4
|
203
|
+
summary: NDR Lookup library
|
204
|
+
test_files: []
|