google-cloud-datastore 0.20.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/lib/google-cloud-datastore.rb +141 -0
- data/lib/google/cloud/datastore.rb +532 -0
- data/lib/google/cloud/datastore/commit.rb +150 -0
- data/lib/google/cloud/datastore/credentials.rb +38 -0
- data/lib/google/cloud/datastore/cursor.rb +79 -0
- data/lib/google/cloud/datastore/dataset.rb +667 -0
- data/lib/google/cloud/datastore/dataset/lookup_results.rb +222 -0
- data/lib/google/cloud/datastore/dataset/query_results.rb +389 -0
- data/lib/google/cloud/datastore/entity.rb +454 -0
- data/lib/google/cloud/datastore/errors.rb +43 -0
- data/lib/google/cloud/datastore/gql_query.rb +216 -0
- data/lib/google/cloud/datastore/grpc_utils.rb +140 -0
- data/lib/google/cloud/datastore/key.rb +289 -0
- data/lib/google/cloud/datastore/properties.rb +133 -0
- data/lib/google/cloud/datastore/query.rb +351 -0
- data/lib/google/cloud/datastore/service.rb +171 -0
- data/lib/google/cloud/datastore/transaction.rb +365 -0
- data/lib/google/cloud/datastore/version.rb +22 -0
- data/lib/google/datastore/v1/datastore_pb.rb +120 -0
- data/lib/google/datastore/v1/datastore_services_pb.rb +61 -0
- data/lib/google/datastore/v1/entity_pb.rb +63 -0
- data/lib/google/datastore/v1/query_pb.rb +131 -0
- metadata +236 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
# Copyright 2016 Google Inc. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
|
16
|
+
require "google/cloud/datastore/credentials"
|
17
|
+
require "google/datastore/v1/datastore_services_pb"
|
18
|
+
require "google/cloud/core/grpc_backoff"
|
19
|
+
|
20
|
+
module Google
|
21
|
+
module Cloud
|
22
|
+
module Datastore
|
23
|
+
##
|
24
|
+
# @private Represents the gRPC Datastore service, including all the API
|
25
|
+
# methods.
|
26
|
+
class Service
|
27
|
+
attr_accessor :project, :credentials, :host, :retries, :timeout
|
28
|
+
|
29
|
+
##
|
30
|
+
# Creates a new Service instance.
|
31
|
+
def initialize project, credentials, host: nil, retries: nil,
|
32
|
+
timeout: nil
|
33
|
+
@project = project
|
34
|
+
@credentials = credentials
|
35
|
+
@host = host || "datastore.googleapis.com"
|
36
|
+
@retries = retries
|
37
|
+
@timeout = timeout
|
38
|
+
end
|
39
|
+
|
40
|
+
def creds
|
41
|
+
return credentials if insecure?
|
42
|
+
GRPC::Core::ChannelCredentials.new.compose \
|
43
|
+
GRPC::Core::CallCredentials.new credentials.client.updater_proc
|
44
|
+
end
|
45
|
+
|
46
|
+
def datastore
|
47
|
+
return mocked_datastore if mocked_datastore
|
48
|
+
@datastore ||= Google::Datastore::V1::Datastore::Stub.new(
|
49
|
+
host, creds, timeout: timeout)
|
50
|
+
end
|
51
|
+
attr_accessor :mocked_datastore
|
52
|
+
|
53
|
+
def insecure?
|
54
|
+
credentials == :this_channel_is_insecure
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Allocate IDs for incomplete keys.
|
59
|
+
# (This is useful for referencing an entity before it is inserted.)
|
60
|
+
def allocate_ids *incomplete_keys
|
61
|
+
allocate_req = Google::Datastore::V1::AllocateIdsRequest.new(
|
62
|
+
project_id: project,
|
63
|
+
keys: incomplete_keys
|
64
|
+
)
|
65
|
+
|
66
|
+
execute { datastore.allocate_ids allocate_req }
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Look up entities by keys.
|
71
|
+
def lookup *keys, consistency: nil, transaction: nil
|
72
|
+
lookup_req = Google::Datastore::V1::LookupRequest.new(
|
73
|
+
project_id: project,
|
74
|
+
keys: keys
|
75
|
+
)
|
76
|
+
lookup_req.read_options = generate_read_options consistency,
|
77
|
+
transaction
|
78
|
+
|
79
|
+
execute { datastore.lookup lookup_req }
|
80
|
+
end
|
81
|
+
|
82
|
+
# Query for entities.
|
83
|
+
def run_query query, namespace = nil, consistency: nil, transaction: nil
|
84
|
+
run_req = Google::Datastore::V1::RunQueryRequest.new(
|
85
|
+
project_id: project)
|
86
|
+
if query.is_a? Google::Datastore::V1::Query
|
87
|
+
run_req["query"] = query
|
88
|
+
elsif query.is_a? Google::Datastore::V1::GqlQuery
|
89
|
+
run_req["gql_query"] = query
|
90
|
+
else
|
91
|
+
fail ArgumentError, "Unable to query with a #{query.class} object."
|
92
|
+
end
|
93
|
+
run_req.read_options = generate_read_options consistency, transaction
|
94
|
+
|
95
|
+
run_req.partition_id = Google::Datastore::V1::PartitionId.new(
|
96
|
+
namespace_id: namespace) if namespace
|
97
|
+
|
98
|
+
execute { datastore.run_query run_req }
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Begin a new transaction.
|
103
|
+
def begin_transaction
|
104
|
+
tx_req = Google::Datastore::V1::BeginTransactionRequest.new(
|
105
|
+
project_id: project
|
106
|
+
)
|
107
|
+
|
108
|
+
execute { datastore.begin_transaction tx_req }
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Commit a transaction, optionally creating, deleting or modifying
|
113
|
+
# some entities.
|
114
|
+
def commit mutations, transaction: nil
|
115
|
+
commit_req = Google::Datastore::V1::CommitRequest.new(
|
116
|
+
project_id: project,
|
117
|
+
mode: :NON_TRANSACTIONAL,
|
118
|
+
mutations: mutations
|
119
|
+
)
|
120
|
+
if transaction
|
121
|
+
commit_req.mode = :TRANSACTIONAL
|
122
|
+
commit_req.transaction = transaction
|
123
|
+
end
|
124
|
+
|
125
|
+
execute { datastore.commit commit_req }
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Roll back a transaction.
|
130
|
+
def rollback transaction
|
131
|
+
rb_req = Google::Datastore::V1::RollbackRequest.new(
|
132
|
+
project_id: project,
|
133
|
+
transaction: transaction
|
134
|
+
)
|
135
|
+
|
136
|
+
execute { datastore.rollback rb_req }
|
137
|
+
end
|
138
|
+
|
139
|
+
def inspect
|
140
|
+
"#{self.class}(#{@project})"
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Performs backoff and error handling
|
145
|
+
def execute
|
146
|
+
Google::Cloud::Core::GrpcBackoff.new(retries: retries).execute do
|
147
|
+
yield
|
148
|
+
end
|
149
|
+
rescue GRPC::BadStatus => e
|
150
|
+
raise Google::Cloud::Error.from_error(e)
|
151
|
+
end
|
152
|
+
|
153
|
+
protected
|
154
|
+
|
155
|
+
def generate_read_options consistency, transaction
|
156
|
+
if consistency == :eventual
|
157
|
+
return Google::Datastore::V1::ReadOptions.new(
|
158
|
+
read_consistency: :EVENTUAL)
|
159
|
+
elsif consistency == :strong
|
160
|
+
return Google::Datastore::V1::ReadOptions.new(
|
161
|
+
read_consistency: :STRONG)
|
162
|
+
elsif transaction
|
163
|
+
return Google::Datastore::V1::ReadOptions.new(
|
164
|
+
transaction: transaction)
|
165
|
+
end
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,365 @@
|
|
1
|
+
# Copyright 2014 Google Inc. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
|
16
|
+
module Google
|
17
|
+
module Cloud
|
18
|
+
module Datastore
|
19
|
+
##
|
20
|
+
# # Transaction
|
21
|
+
#
|
22
|
+
# Special Connection instance for running transactions.
|
23
|
+
#
|
24
|
+
# See {Google::Cloud::Datastore::Dataset#transaction}
|
25
|
+
#
|
26
|
+
# @see https://cloud.google.com/datastore/docs/concepts/transactions
|
27
|
+
# Transactions
|
28
|
+
#
|
29
|
+
# @example Transactional update:
|
30
|
+
# def transfer_funds from_key, to_key, amount
|
31
|
+
# datastore.transaction do |tx|
|
32
|
+
# from = tx.find from_key
|
33
|
+
# from["balance"] -= amount
|
34
|
+
# to = tx.find to_key
|
35
|
+
# to["balance"] += amount
|
36
|
+
# tx.save from, to
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @example Retry logic using the transactional update example above:
|
41
|
+
# (1..5).each do |i|
|
42
|
+
# begin
|
43
|
+
# return transfer_funds from_key, to_key, amount
|
44
|
+
# rescue Google::Cloud::Error => e
|
45
|
+
# raise e if i == 5
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# @example Transactional read:
|
50
|
+
# task_list_key = datastore.key "TaskList", "default"
|
51
|
+
# datastore.transaction do |tx|
|
52
|
+
# task_list = tx.find task_list_key
|
53
|
+
# query = tx.query("Task").ancestor(task_list)
|
54
|
+
# tasks_in_list = tx.run query
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
class Transaction < Dataset
|
58
|
+
attr_reader :id
|
59
|
+
|
60
|
+
##
|
61
|
+
# @private Creates a new Transaction instance.
|
62
|
+
# Takes a Connection and Service instead of project and Credentials.
|
63
|
+
def initialize service
|
64
|
+
@service = service
|
65
|
+
reset!
|
66
|
+
start
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Persist entities in a transaction.
|
71
|
+
#
|
72
|
+
# @example Transactional get or create:
|
73
|
+
# task_key = datastore.key "Task", "sampleTask"
|
74
|
+
#
|
75
|
+
# task = nil
|
76
|
+
# datastore.transaction do |tx|
|
77
|
+
# task = tx.find task_key
|
78
|
+
# if task.nil?
|
79
|
+
# task = datastore.entity task_key do |t|
|
80
|
+
# t["type"] = "Personal"
|
81
|
+
# t["done"] = false
|
82
|
+
# t["priority"] = 4
|
83
|
+
# t["description"] = "Learn Cloud Datastore"
|
84
|
+
# end
|
85
|
+
# tx.save task
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
def save *entities
|
90
|
+
@commit.save(*entities)
|
91
|
+
# Do not save yet
|
92
|
+
entities
|
93
|
+
end
|
94
|
+
alias_method :upsert, :save
|
95
|
+
|
96
|
+
##
|
97
|
+
# Insert entities in a transaction. An InvalidArgumentError will raised
|
98
|
+
# if the entities cannot be inserted.
|
99
|
+
#
|
100
|
+
# @example Transactional insert:
|
101
|
+
# task_key = datastore.key "Task", "sampleTask"
|
102
|
+
#
|
103
|
+
# task = nil
|
104
|
+
# datastore.transaction do |tx|
|
105
|
+
# task = tx.find task_key
|
106
|
+
# if task.nil?
|
107
|
+
# task = datastore.entity task_key do |t|
|
108
|
+
# t["type"] = "Personal"
|
109
|
+
# t["done"] = false
|
110
|
+
# t["priority"] = 4
|
111
|
+
# t["description"] = "Learn Cloud Datastore"
|
112
|
+
# end
|
113
|
+
# tx.insert task
|
114
|
+
# end
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
def insert *entities
|
118
|
+
@commit.insert(*entities)
|
119
|
+
# Do not insert yet
|
120
|
+
entities
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Update entities in a transaction. An InvalidArgumentError will raised
|
125
|
+
# if the entities cannot be updated.
|
126
|
+
#
|
127
|
+
# @example Transactional update:
|
128
|
+
# task_key = datastore.key "Task", "sampleTask"
|
129
|
+
#
|
130
|
+
# task = nil
|
131
|
+
# datastore.transaction do |tx|
|
132
|
+
# task = tx.find task_key
|
133
|
+
# if task
|
134
|
+
# task["done"] = true
|
135
|
+
# tx.update task
|
136
|
+
# end
|
137
|
+
# end
|
138
|
+
#
|
139
|
+
def update *entities
|
140
|
+
@commit.update(*entities)
|
141
|
+
# Do not update yet
|
142
|
+
entities
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Remove entities in a transaction.
|
147
|
+
#
|
148
|
+
# @example
|
149
|
+
# datastore.transaction do |tx|
|
150
|
+
# if tx.find(task_list.key).nil?
|
151
|
+
# tx.delete task1, task2
|
152
|
+
# end
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
def delete *entities_or_keys
|
156
|
+
@commit.delete(*entities_or_keys)
|
157
|
+
# Do not delete yet
|
158
|
+
true
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Retrieve an entity by providing key information. The lookup is run
|
163
|
+
# within the transaction.
|
164
|
+
#
|
165
|
+
# @param [Key, String] key_or_kind A Key object or `kind` string value.
|
166
|
+
#
|
167
|
+
# @return [Google::Cloud::Datastore::Entity, nil]
|
168
|
+
#
|
169
|
+
# @example Finding an entity with a key:
|
170
|
+
# task_key = datastore.key "Task", "sampleTask"
|
171
|
+
# task = datastore.find task_key
|
172
|
+
#
|
173
|
+
# @example Finding an entity with a `kind` and `id`/`name`:
|
174
|
+
# task = datastore.find "Task", "sampleTask"
|
175
|
+
#
|
176
|
+
def find key_or_kind, id_or_name = nil
|
177
|
+
key = key_or_kind
|
178
|
+
unless key.is_a? Google::Cloud::Datastore::Key
|
179
|
+
key = Key.new key_or_kind, id_or_name
|
180
|
+
end
|
181
|
+
find_all(key).first
|
182
|
+
end
|
183
|
+
alias_method :get, :find
|
184
|
+
|
185
|
+
##
|
186
|
+
# Retrieve the entities for the provided keys. The lookup is run within
|
187
|
+
# the transaction.
|
188
|
+
#
|
189
|
+
# @param [Key] keys One or more Key objects to find records for.
|
190
|
+
#
|
191
|
+
# @return [Google::Cloud::Datastore::Dataset::LookupResults]
|
192
|
+
#
|
193
|
+
# @example
|
194
|
+
# gcloud = Google::Cloud.new
|
195
|
+
# datastore = gcloud.datastore
|
196
|
+
# task_key1 = datastore.key "Task", 123456
|
197
|
+
# task_key2 = datastore.key "Task", 987654
|
198
|
+
# tasks = datastore.find_all task_key1, task_key2
|
199
|
+
#
|
200
|
+
def find_all *keys
|
201
|
+
ensure_service!
|
202
|
+
lookup_res = service.lookup(*Array(keys).flatten.map(&:to_grpc),
|
203
|
+
transaction: @id)
|
204
|
+
LookupResults.from_grpc lookup_res, service, nil, @id
|
205
|
+
end
|
206
|
+
alias_method :lookup, :find_all
|
207
|
+
|
208
|
+
##
|
209
|
+
# Retrieve entities specified by a Query. The query is run within the
|
210
|
+
# transaction.
|
211
|
+
#
|
212
|
+
# @param [Query] query The Query object with the search criteria.
|
213
|
+
# @param [String] namespace The namespace the query is to run within.
|
214
|
+
#
|
215
|
+
# @return [Google::Cloud::Datastore::Dataset::QueryResults]
|
216
|
+
#
|
217
|
+
# @example
|
218
|
+
# query = datastore.query("Task").
|
219
|
+
# where("done", "=", false)
|
220
|
+
# datastore.transaction do |tx|
|
221
|
+
# tasks = tx.run query
|
222
|
+
# end
|
223
|
+
#
|
224
|
+
# @example Run the query within a namespace with the `namespace` option:
|
225
|
+
# query = Google::Cloud::Datastore::Query.new.kind("Task").
|
226
|
+
# where("done", "=", false)
|
227
|
+
# datastore.transaction do |tx|
|
228
|
+
# tasks = tx.run query, namespace: "ns~todo-project"
|
229
|
+
# end
|
230
|
+
#
|
231
|
+
def run query, namespace: nil
|
232
|
+
ensure_service!
|
233
|
+
unless query.is_a?(Query) || query.is_a?(GqlQuery)
|
234
|
+
fail ArgumentError, "Cannot run a #{query.class} object."
|
235
|
+
end
|
236
|
+
query_res = service.run_query query.to_grpc, namespace,
|
237
|
+
transaction: @id
|
238
|
+
QueryResults.from_grpc query_res, service, namespace,
|
239
|
+
query.to_grpc.dup
|
240
|
+
end
|
241
|
+
alias_method :run_query, :run
|
242
|
+
|
243
|
+
##
|
244
|
+
# Begins a transaction.
|
245
|
+
# This method is run when a new Transaction is created.
|
246
|
+
def start
|
247
|
+
fail TransactionError, "Transaction already opened." unless @id.nil?
|
248
|
+
|
249
|
+
ensure_service!
|
250
|
+
tx_res = service.begin_transaction
|
251
|
+
@id = tx_res.transaction
|
252
|
+
end
|
253
|
+
alias_method :begin_transaction, :start
|
254
|
+
|
255
|
+
##
|
256
|
+
# Commits a transaction.
|
257
|
+
#
|
258
|
+
# @yield [commit] an optional block for making changes
|
259
|
+
# @yieldparam [Commit] commit The object that changes are made on
|
260
|
+
#
|
261
|
+
# @example
|
262
|
+
# require "google/cloud"
|
263
|
+
#
|
264
|
+
# gcloud = Google::Cloud.new
|
265
|
+
# datastore = gcloud.datastore
|
266
|
+
#
|
267
|
+
# task = datastore.entity "Task" do |t|
|
268
|
+
# t["type"] = "Personal"
|
269
|
+
# t["done"] = false
|
270
|
+
# t["priority"] = 4
|
271
|
+
# t["description"] = "Learn Cloud Datastore"
|
272
|
+
# end
|
273
|
+
#
|
274
|
+
# tx = datastore.transaction
|
275
|
+
# begin
|
276
|
+
# if tx.find(task.key).nil?
|
277
|
+
# tx.save task
|
278
|
+
# end
|
279
|
+
# tx.commit
|
280
|
+
# rescue
|
281
|
+
# tx.rollback
|
282
|
+
# end
|
283
|
+
#
|
284
|
+
# @example Commit can be passed a block, same as {Dataset#commit}:
|
285
|
+
# require "google/cloud"
|
286
|
+
#
|
287
|
+
# gcloud = Google::Cloud.new
|
288
|
+
# datastore = gcloud.datastore
|
289
|
+
#
|
290
|
+
# tx = datastore.transaction
|
291
|
+
# begin
|
292
|
+
# tx.commit do |c|
|
293
|
+
# c.save task1, task2
|
294
|
+
# c.delete entity1, entity2
|
295
|
+
# end
|
296
|
+
# rescue
|
297
|
+
# tx.rollback
|
298
|
+
# end
|
299
|
+
#
|
300
|
+
def commit
|
301
|
+
fail TransactionError,
|
302
|
+
"Cannot commit when not in a transaction." if @id.nil?
|
303
|
+
|
304
|
+
yield @commit if block_given?
|
305
|
+
|
306
|
+
ensure_service!
|
307
|
+
|
308
|
+
commit_res = service.commit @commit.mutations, transaction: @id
|
309
|
+
entities = @commit.entities
|
310
|
+
returned_keys = commit_res.mutation_results.map(&:key)
|
311
|
+
returned_keys.each_with_index do |key, index|
|
312
|
+
next if entities[index].nil?
|
313
|
+
entities[index].key = Key.from_grpc(key) unless key.nil?
|
314
|
+
end
|
315
|
+
# Make sure all entity keys are frozen so all show as persisted
|
316
|
+
entities.each { |e| e.key.freeze unless e.persisted? }
|
317
|
+
true
|
318
|
+
end
|
319
|
+
|
320
|
+
##
|
321
|
+
# Rolls a transaction back.
|
322
|
+
#
|
323
|
+
# @example
|
324
|
+
# require "google/cloud"
|
325
|
+
#
|
326
|
+
# gcloud = Google::Cloud.new
|
327
|
+
# datastore = gcloud.datastore
|
328
|
+
#
|
329
|
+
# task = datastore.entity "Task" do |t|
|
330
|
+
# t["type"] = "Personal"
|
331
|
+
# t["done"] = false
|
332
|
+
# t["priority"] = 4
|
333
|
+
# t["description"] = "Learn Cloud Datastore"
|
334
|
+
# end
|
335
|
+
#
|
336
|
+
# tx = datastore.transaction
|
337
|
+
# begin
|
338
|
+
# if tx.find(task.key).nil?
|
339
|
+
# tx.save task
|
340
|
+
# end
|
341
|
+
# tx.commit
|
342
|
+
# rescue
|
343
|
+
# tx.rollback
|
344
|
+
# end
|
345
|
+
def rollback
|
346
|
+
if @id.nil?
|
347
|
+
fail TransactionError, "Cannot rollback when not in a transaction."
|
348
|
+
end
|
349
|
+
|
350
|
+
ensure_service!
|
351
|
+
service.rollback @id
|
352
|
+
true
|
353
|
+
end
|
354
|
+
|
355
|
+
##
|
356
|
+
# Reset the transaction.
|
357
|
+
# {Transaction#start} must be called afterwards.
|
358
|
+
def reset!
|
359
|
+
@id = nil
|
360
|
+
@commit = Commit.new
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|