/
ivanstrike
/
tasker
Обзор
Документация
Войти
/
ivanstrike
/
tasker
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Lab3/Client/TaskApiClient.cs
443 строки
14 KB
ivanstrike
Lab3
13 дек 2025, 11:15
13 дек 2025, 11:15
15c0fa5
Код
Авторство
О чём код?
using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using TaskApiClient.Models; namespace TaskApiClient; /// <summary> /// Клиент для взаимодействия с Task API (python_Sklyarenko) /// Поддерживает: /// - JWT аутентификацию /// - X-API-Key аутентификацию (Lab2/v3) /// - Версии API v1, v2 и v3 /// - Идемпотентность через Idempotency-Key /// - Rate limiting (отслеживание X-Limit-Remaining) /// - Sparse fieldsets (параметр include) /// - Обработку ошибок /// </summary> public class TaskApiClient : IDisposable { private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonOptions; private string? _accessToken; private string? _apiKey; public RateLimitInfo RateLimit { get; private set; } = new(); public TaskApiClient(string baseUrl = "http://localhost:8000", string? apiKey = null) { _httpClient = new HttpClient { BaseAddress = new Uri(baseUrl) }; _apiKey = apiKey; if (!string.IsNullOrEmpty(_apiKey)) { _httpClient.DefaultRequestHeaders.Add("X-API-Key", _apiKey); } _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) } }; } #region Authentication /// <summary> /// Регистрация нового пользователя /// </summary> public async Task<ApiResponse<UserOut>> RegisterAsync(UserCreate user) { return await PostAsync<UserCreate, UserOut>("/api/v1/auth/register", user); } /// <summary> /// Вход в систему (получение JWT токена) /// </summary> public async Task<ApiResponse<Token>> LoginAsync(string email, string password) { var request = new LoginRequest(email, password); var response = await PostAsync<LoginRequest, Token>("/api/v1/auth/login", request); if (response.IsSuccess && response.Data != null) { _accessToken = response.Data.AccessToken; _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); } return response; } /// <summary> /// Выход из системы (очистка токена) /// </summary> public void Logout() { _accessToken = null; _httpClient.DefaultRequestHeaders.Authorization = null; } /// <summary> /// Получение токена через форму (API v3 - Lab2) /// </summary> public async Task<ApiResponse<TokenResponseV3>> GetTokenV3Async(string username) { var formData = new Dictionary<string, string> { { "username", username } }; var content = new FormUrlEncodedContent(formData); var response = await _httpClient.PostAsync("/auth/token", content); var result = await ProcessResponse<TokenResponseV3>(response); if (result.IsSuccess && result.Data != null) { _accessToken = result.Data.AccessToken; _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); } return result; } #endregion #region Projects (V1) /// <summary> /// Получить список проектов текущего пользователя /// </summary> public async Task<ApiResponse<List<ProjectOut>>> GetProjectsAsync() { return await GetAsync<List<ProjectOut>>("/api/v1/projects/"); } /// <summary> /// Получить проект по ID /// </summary> public async Task<ApiResponse<ProjectOut>> GetProjectAsync(int projectId) { return await GetAsync<ProjectOut>($"/api/v1/projects/{projectId}"); } /// <summary> /// Создать новый проект (с поддержкой идемпотентности) /// </summary> public async Task<ApiResponse<ProjectOut>> CreateProjectAsync( ProjectCreate project, string? idempotencyKey = null) { return await PostAsync<ProjectCreate, ProjectOut>( "/api/v1/projects/", project, idempotencyKey); } /// <summary> /// Обновить проект /// </summary> public async Task<ApiResponse<ProjectOut>> UpdateProjectAsync( int projectId, ProjectUpdate update) { return await PatchAsync<ProjectUpdate, ProjectOut>( $"/api/v1/projects/{projectId}", update); } /// <summary> /// Удалить проект /// </summary> public async Task<ApiResponse<object?>> DeleteProjectAsync(int projectId) { return await DeleteAsync($"/api/v1/projects/{projectId}"); } #endregion #region Tasks V1 /// <summary> /// Получить список задач (V1) /// </summary> public async Task<ApiResponse<List<TaskOutV1>>> GetTasksV1Async(int? projectId = null) { var url = "/api/v1/tasks"; if (projectId.HasValue) url += $"?project_id={projectId.Value}"; return await GetAsync<List<TaskOutV1>>(url); } /// <summary> /// Создать задачу (V1, с идемпотентностью) /// </summary> public async Task<ApiResponse<TaskOutV1>> CreateTaskV1Async( TaskCreateV1 task, string? idempotencyKey = null) { return await PostAsync<TaskCreateV1, TaskOutV1>( "/api/v1/tasks/", task, idempotencyKey); } #endregion #region Tasks V2 /// <summary> /// Получить список задач (V2 - с фильтром по приоритету) /// </summary> public async Task<ApiResponse<List<TaskOutV2>>> GetTasksV2Async( int? projectId = null, TaskPriority? priority = null) { var url = "/api/v2/tasks"; var queryParams = new List<string>(); if (projectId.HasValue) queryParams.Add($"project_id={projectId.Value}"); if (priority.HasValue) queryParams.Add($"priority={priority.Value.ToString().ToLower()}"); if (queryParams.Any()) url += "?" + string.Join("&", queryParams); return await GetAsync<List<TaskOutV2>>(url); } /// <summary> /// Создать задачу (V2 - с приоритетом и идемпотентностью) /// </summary> public async Task<ApiResponse<TaskOutV2>> CreateTaskV2Async( TaskCreateV2 task, string? idempotencyKey = null) { return await PostAsync<TaskCreateV2, TaskOutV2>( "/api/v2/tasks/", task, idempotencyKey); } #endregion #region API V3 - Lab2 /// <summary> /// Получить список проектов с пагинацией и sparse fieldsets (API v3) /// </summary> /// <param name="offset">Смещение для пагинации</param> /// <param name="limit">Количество элементов (по умолчанию 10)</param> /// <param name="include">Поля для включения (через запятую), например "id,name"</param> public async Task<ApiResponse<List<ProjectV3>>> GetProjectsV3Async( int offset = 0, int limit = 10, string? include = null) { var url = $"/api/v3/projects?offset={offset}&limit={limit}"; if (!string.IsNullOrEmpty(include)) url += $"&include={Uri.EscapeDataString(include)}"; return await GetAsync<List<ProjectV3>>(url); } /// <summary> /// Получить список задач с пагинацией и sparse fieldsets (API v3) /// </summary> /// <param name="offset">Смещение для пагинации</param> /// <param name="limit">Количество элементов (по умолчанию 10)</param> /// <param name="include">Поля для включения (через запятую), например "id,title,status"</param> public async Task<ApiResponse<List<TaskV3>>> GetTasksV3Async( int offset = 0, int limit = 10, string? include = null) { var url = $"/api/v3/tasks?offset={offset}&limit={limit}"; if (!string.IsNullOrEmpty(include)) url += $"&include={Uri.EscapeDataString(include)}"; return await GetAsync<List<TaskV3>>(url); } #endregion #region HTTP Helpers private async Task<ApiResponse<TResponse>> GetAsync<TResponse>(string url) { try { var response = await _httpClient.GetAsync(url); return await ProcessResponse<TResponse>(response); } catch (Exception ex) { return CreateErrorResponse<TResponse>(ex); } } private async Task<ApiResponse<TResponse>> PostAsync<TRequest, TResponse>( string url, TRequest data, string? idempotencyKey = null) { try { var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonContent.Create(data, options: _jsonOptions) }; if (!string.IsNullOrEmpty(idempotencyKey)) { request.Headers.Add("Idempotency-Key", idempotencyKey); } var response = await _httpClient.SendAsync(request); return await ProcessResponse<TResponse>(response); } catch (Exception ex) { return CreateErrorResponse<TResponse>(ex); } } private async Task<ApiResponse<TResponse>> PatchAsync<TRequest, TResponse>( string url, TRequest data) { try { var request = new HttpRequestMessage(HttpMethod.Patch, url) { Content = JsonContent.Create(data, options: _jsonOptions) }; var response = await _httpClient.SendAsync(request); return await ProcessResponse<TResponse>(response); } catch (Exception ex) { return CreateErrorResponse<TResponse>(ex); } } private async Task<ApiResponse<object?>> DeleteAsync(string url) { try { var response = await _httpClient.DeleteAsync(url); return await ProcessResponse<object?>(response); } catch (Exception ex) { return CreateErrorResponse<object?>(ex); } } private async Task<ApiResponse<T>> ProcessResponse<T>(HttpResponseMessage response) { // Извлечение заголовков rate limiting UpdateRateLimitInfo(response.Headers); var headers = response.Headers.ToDictionary( h => h.Key, h => string.Join(", ", h.Value) ); var statusCode = (int)response.StatusCode; if (response.IsSuccessStatusCode) { T? data = default; // Для 204 No Content не пытаемся десериализовать if (response.StatusCode != HttpStatusCode.NoContent && response.Content.Headers.ContentLength > 0) { data = await response.Content.ReadFromJsonAsync<T>(_jsonOptions); } return new ApiResponse<T> { Data = data, IsSuccess = true, StatusCode = statusCode, Headers = headers }; } else { string? errorMessage = null; try { var errorContent = await response.Content.ReadAsStringAsync(); var errorDoc = JsonDocument.Parse(errorContent); if (errorDoc.RootElement.TryGetProperty("detail", out var detail)) { errorMessage = detail.GetString(); } else { errorMessage = errorContent; } } catch { errorMessage = response.ReasonPhrase; } return new ApiResponse<T> { IsSuccess = false, ErrorMessage = errorMessage, StatusCode = statusCode, Headers = headers }; } } private void UpdateRateLimitInfo(HttpResponseHeaders headers) { if (headers.TryGetValues("X-Limit-Remaining", out var remaining)) { if (int.TryParse(remaining.FirstOrDefault(), out var remainingValue)) { RateLimit.Remaining = remainingValue; } } if (headers.TryGetValues("Retry-After", out var retryAfter)) { if (int.TryParse(retryAfter.FirstOrDefault(), out var retryAfterValue)) { RateLimit.RetryAfter = retryAfterValue; } } } private ApiResponse<T> CreateErrorResponse<T>(Exception ex) { return new ApiResponse<T> { IsSuccess = false, ErrorMessage = ex.Message, StatusCode = 0, Headers = new Dictionary<string, string>() }; } #endregion public void Dispose() { _httpClient?.Dispose(); } }