Git Product home page Git Product logo

Comments (8)

juse-less avatar juse-less commented on June 2, 2024 1

Very strange!
I'd expect it to result in a HTTP 400 since the endpoint itself can handle POST.

I guess you could create a custom OAuth Request that uses the expected headers.

I don't remember off the top of my head, but if the OAuth specification (or at least Client Credentials Grant) states that form data has to be used, then Saloon could enforce it.
But with the possibility to override, since some APIs don't follow various specifications.

from saloon.

Sammyjo20 avatar Sammyjo20 commented on June 2, 2024

Hey @labomatik I'm sorry for the delay in getting back to you and thank you very much for the GitHub Sponsor! It means a lot. 405 usually means that the HTTP status code is wrong - the getAccessToken method usually uses a POST request. Does the API you are integrating with require a different HTTP status code? Maybe PUT/PATCH?

from saloon.

labomatik avatar labomatik commented on June 2, 2024

Hello, No problem for the delay :-) It's an open source package

I've tried this already in postman to make sure the API is not using another HTTP request

POST https://auth.acc.connect.easypost.eu/oauth2/token?grant_type=client_credentials&scope=connect%2Fread%3Ajobs+connect%2Fsubmit%3Ajobs+connect%2Fread%3Asending-events

The result is a correct json {"error":"invalid_client"} .

This is also available in the API doc: https://documentation.acc.connect.easypost.eu/static/index.html

Somehow it's not working with the request i'm doing with saloon.
I guess it's something stupid i didn't configured...

from saloon.

labomatik avatar labomatik commented on June 2, 2024

I've tried the new ->debug() with no luck...
The result is the same, seems like saloon didn't execute the request.

from saloon.

juse-less avatar juse-less commented on June 2, 2024

@labomatik Hi!

Have you tried wrapping the getAccessToken() in try-catch for the RequestException, and then dump the Request, and PendingRequest?
It'd be interesting to see which Methods they show, since HTTP 405 means that the HTTP method used is not allowed.
Also do dump the headers on the Response, because they must return an Allow header with the HTTP methods allowed.

Would it also be possible to get some more of the Connector code, as well as a full stack trace?
I can't see anything immediately obvious in the Saloon code that'd cause this.
So any code and details you can provide would help.

from saloon.

labomatik avatar labomatik commented on June 2, 2024

This is the trace:


App\Http\Integrations\EasyPost\EasyPostConnector#1
(
    [*:authenticator] => null
    [*:headers] => Saloon\Repositories\ArrayStore#2
    (
        [*:data] => [        
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ]
    )
    [*:query] => Saloon\Repositories\ArrayStore#3
    (
        [*:data] => [],
    )
    [*:config] => Saloon\Repositories\ArrayStore#4
    (
        [*:data] => [],
    )
    [*:middlewarePipeline] => Saloon\Helpers\MiddlewarePipeline#5
    (
        [*:requestPipeline] => Saloon\Helpers\Pipeline#6
        (
            [*:pipes] => [],
        )
        [*:responsePipeline] => Saloon\Helpers\Pipeline#7
        (
            [*:pipes] => [],
        )
    )
    [*:delay] => Saloon\Repositories\IntegerStore#8
    (
        [*:data] => null
    )
    [*:response] => null
    [*:mockClient] => null
    [*:defaultSender] => ''
    [*:sender] => Saloon\Http\Senders\GuzzleSender#9
    (
        [*:client] => GuzzleHttp\Client#10
        (
            [GuzzleHttp\Client:config] => [            
                'crypto_method' => 33,
                'connect_timeout' => 10,
                'timeout' => 30,
                'http_errors' => true,
                'handler' => GuzzleHttp\HandlerStack#11
                (
                    [GuzzleHttp\HandlerStack:handler] => Closure(...)
                    [GuzzleHttp\HandlerStack:stack] => [...],
                    [GuzzleHttp\HandlerStack:cached] => Closure(...)
                ),
                'allow_redirects' => [                
                    'max' => 5,
                    'protocols' => [...],,
                    'strict' => false,
                    'referer' => false,
                    'track_redirects' => false,
                ],
                'decode_content' => true,
                'verify' => true,
                'cookies' => false,
                'idn_conversion' => false,
                'headers' => [                
                    'User-Agent' => 'GuzzleHttp/7',
                ],
            ]
        )
        [*:handlerStack] => GuzzleHttp\HandlerStack#11(...)
    )
    [tries] => null
    [retryInterval] => null
    [useExponentialBackoff] => null
    [throwOnMaxTries] => null
    [*:connectTimeout] => 60
    [*:requestTimeout] => 120
    [*:oauthConfig] => Saloon\Helpers\OAuth2\OAuthConfig#12
    (
        [*:clientId] => 'qsd'
        [*:clientSecret] => 'sd'
        [*:redirectUri] => ''
        [*:authorizeEndpoint] => 'authorize'
        [*:tokenEndpoint] => 'https://auth.acc.connect.easypost.eu/oauth2/token'
        [*:userEndpoint] => 'user'
        [*:requestModifier] => null
        [*:defaultScopes] => [        
            0 => 'connect/read:jobs',
            1 => 'connect/submit:jobs',
            2 => 'connect/read:sending-events',
        ]
    )
)


Saloon\Exceptions\Request\Statuses\MethodNotAllowedException#1
(
    [*:message] => 'Method Not Allowed (405) Response: '
    [Exception:string] => ''
    [*:code] => 0
    [*:file] => 'vendor/saloonphp/saloon/src/Helpers/RequestExceptionHelper.php'
    [*:line] => 51
    [Exception:trace] => [    
        0 => [        
            'file' => 'vendor/saloonphp/saloon/src/Http/Response.php',
            'line' => 450,
            'function' => 'create',
            'class' => 'Saloon\\Helpers\\RequestExceptionHelper',
            'type' => '::',
            'args' => [            
                0 => Saloon\Http\Response#2
                (
                    [*:psrRequest] => GuzzleHttp\Psr7\Request(...)
                    [*:psrResponse] => GuzzleHttp\Psr7\Response(...)
                    [*:pendingRequest] => Saloon\Http\PendingRequest(...)
                    [*:senderException] => GuzzleHttp\Exception\ClientException(...)
                    [*:mocked] => false
                    [*:cached] => false
                    [*:fakeResponse] => null
                ),
                1 => GuzzleHttp\Exception\ClientException#3
                (
                    [*:message] => 'Client error: `POST https://auth.acc.connect.easypost.eu/oauth2/token` resulted in a `405 Method Not Allowed` response'
                    [Exception:string] => ''
                    [*:code] => 405
                    [*:file] => 'vendor/guzzlehttp/guzzle/src/Exception/RequestException.php'
                    [*:line] => 113
                    [Exception:trace] => [...],
                    [Exception:previous] => null
                    [GuzzleHttp\Exception\RequestException:request] => GuzzleHttp\Psr7\Request(...)
                    [GuzzleHttp\Exception\RequestException:response] => GuzzleHttp\Psr7\Response(...)
                    [GuzzleHttp\Exception\RequestException:handlerContext] => [...],
                ),
            ],
        ],
        1 => [        
            'file' => 'vendor/saloonphp/saloon/src/Http/Response.php',
            'line' => 428,
            'function' => 'createException',
            'class' => 'Saloon\\Http\\Response',
            'type' => '->',
            'args' => [],,
        ],
        2 => [        
            'file' => 'vendor/saloonphp/saloon/src/Http/Response.php',
            'line' => 462,
            'function' => 'toException',
            'class' => 'Saloon\\Http\\Response',
            'type' => '->',
            'args' => [],,
        ],
        3 => [        
            'file' => 'vendor/saloonphp/saloon/src/Traits/Plugins/AlwaysThrowOnErrors.php',
            'line' => 23,
            'function' => 'throw',
            'class' => 'Saloon\\Http\\Response',
            'type' => '->',
            'args' => [],,
        ],
        4 => [        
            'file' => 'vendor/saloonphp/saloon/src/Helpers/MiddlewarePipeline.php',
            'line' => 87,
            'function' => 'Saloon\\Traits\\Plugins\\{closure}',
            'class' => 'App\\Http\\Integrations\\EasyPost\\EasyPostConnector',
            'type' => '::',
            'args' => [            
                0 => Saloon\Http\Response#2(...),
            ],
        ],
        5 => [        
            'function' => 'Saloon\\Helpers\\{closure}',
            'class' => 'Saloon\\Helpers\\MiddlewarePipeline',
            'type' => '::',
            'args' => [            
                0 => Saloon\Http\Response#2(...),
            ],
        ],
        6 => [        
            'file' => 'vendor/saloonphp/saloon/src/Helpers/Pipeline.php',
            'line' => 45,
            'function' => 'call_user_func',
            'args' => [            
                0 => Closure#4
                (
                    [0] => Closure#4(...)
                ),
                1 => Saloon\Http\Response#2(...),
            ],
        ],
        7 => [        
            'file' => 'vendor/saloonphp/saloon/src/Helpers/MiddlewarePipeline.php',
            'line' => 108,
            'function' => 'process',
            'class' => 'Saloon\\Helpers\\Pipeline',
            'type' => '->',
            'args' => [            
                0 => Saloon\Http\Response#2(...),
            ],
        ],
        8 => [        
            'file' => 'vendor/saloonphp/saloon/src/Http/PendingRequest.php',
            'line' => 152,
            'function' => 'executeResponsePipeline',
            'class' => 'Saloon\\Helpers\\MiddlewarePipeline',
            'type' => '->',
            'args' => [            
                0 => Saloon\Http\Response#2(...),
            ],
        ],
        9 => [        
            'file' => 'vendor/saloonphp/saloon/src/Traits/Connector/SendsRequests.php',
            'line' => 78,
            'function' => 'executeResponsePipeline',
            'class' => 'Saloon\\Http\\PendingRequest',
            'type' => '->',
            'args' => [            
                0 => Saloon\Http\Response#2(...),
            ],
        ],
        10 => [        
            'file' => 'vendor/saloonphp/saloon/src/Traits/OAuth2/ClientCredentialsGrant.php',
            'line' => 40,
            'function' => 'send',
            'class' => 'Saloon\\Http\\Connector',
            'type' => '->',
            'args' => [            
                0 => Saloon\Http\OAuth2\GetClientCredentialsTokenRequest#5
                (
                    [*:method] => Saloon\Enums\Method(...)
                    [*:authenticator] => null
                    [*:headers] => Saloon\Repositories\ArrayStore(...)
                    [*:query] => Saloon\Repositories\ArrayStore(...)
                    [*:config] => Saloon\Repositories\ArrayStore(...)
                    [*:middlewarePipeline] => Saloon\Helpers\MiddlewarePipeline(...)
                    [*:delay] => Saloon\Repositories\IntegerStore(...)
                    [*:response] => null
                    [*:mockClient] => null
                    [tries] => null
                    [retryInterval] => null
                    [useExponentialBackoff] => null
                    [throwOnMaxTries] => null
                    [*:oauthConfig] => Saloon\Helpers\OAuth2\OAuthConfig(...)
                    [*:scopes] => [...],
                    [*:scopeSeparator] => ' '
                    [*:body] => Saloon\Repositories\Body\FormBodyRepository(...)
                ),
            ],
        ],
        11 => [        
            'file' => 'app/Console/Commands/TEMPEasyPostApi.php',
            'line' => 37,
            'function' => 'getAccessToken',
            'class' => 'App\\Http\\Integrations\\EasyPost\\EasyPostConnector',
            'type' => '->',
            'args' => [            
                0 => [                
                    0 => 'connect/read:jobs',
                    1 => 'connect/submit:jobs',
                    2 => 'connect/read:sending-events',
                ],
            ],
        ],
        12 => [        
            'file' => 'vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php',
            'line' => 36,
            'function' => 'handle',
            'class' => 'App\\Console\\Commands\\TEMPEasyPostApi',
            'type' => '->',
            'args' => [],,
        ],
        13 => [        
            'file' => 'vendor/laravel/framework/src/Illuminate/Container/Util.php',
            'line' => 41,
            'function' => 'Illuminate\\Container\\{closure}',
            'class' => 'Illuminate\\Container\\BoundMethod',
            'type' => '::',
            'args' => [],,
        ],
        14 => [        
            'file' => 'vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php',
            'line' => 93,
            'function' => 'unwrapIfClosure',
            'class' => 'Illuminate\\Container\\Util',
            'type' => '::',
            'args' => [            
                0 => Closure#6
                (
                    [0] => Closure#6(...)
                ),
            ],
        ],
        15 => [        
            'file' => 'vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php',
            'line' => 37,
            'function' => 'callBoundMethod',
            'class' => 'Illuminate\\Container\\BoundMethod',
            'type' => '::',
            'args' => [            
                0 => Illuminate\Foundation\Application#7
                (
                    [*:resolved] => [...],
                    [*:bindings] => [...],
                    [*:methodBindings] => [...],
                    [*:instances] => [...],
                    [*:scopedInstances] => [...],
                    [*:aliases] => [...],
                    [*:abstractAliases] => [...],
                    [*:extenders] => [...],
                    [*:tags] => [...],
                    [*:buildStack] => [...],
                    [*:with] => [...],
                    [contextual] => [...],
                    [*:reboundCallbacks] => [...],
                    [*:globalBeforeResolvingCallbacks] => [...],
                    [*:globalResolvingCallbacks] => [...],
                    [*:globalAfterResolvingCallbacks] => [...],
                    [*:beforeResolvingCallbacks] => [...],
                    [*:resolvingCallbacks] => [...],
                    [*:afterResolvingCallbacks] => [...],
                    [*:basePath] => ''
                    [*:hasBeenBootstrapped] => true
                    [*:booted] => true
                    [*:bootingCallbacks] => [...],
                    [*:bootedCallbacks] => [...],
                    [*:terminatingCallbacks] => [...],
                    [*:serviceProviders] => [...],
                    [*:loadedProviders] => [...],
                    [*:deferredServices] => [...],
                    [*:bootstrapPath] => 'bootstrap'
                    [*:appPath] => null
                    [*:configPath] => null
                    [*:databasePath] => null
                    [*:langPath] => 'lang'
                    [*:publicPath] => null
                    [*:storagePath] => null
                    [*:environmentPath] => null
                    [*:environmentFile] => '.env'
                    [*:isRunningInConsole] => true
                    [*:namespace] => 'App\\'
                    [*:absoluteCachePathPrefixes] => [...],
                ),
                1 => [                
                    0 => App\Console\Commands\TEMPEasyPostApi(...),
                    1 => 'handle',
                ],
                2 => Closure#6(...),
            ],
        ],
        16 => [        
            'file' => 'vendor/laravel/framework/src/Illuminate/Container/Container.php',
            'line' => 662,
            'function' => 'call',
            'class' => 'Illuminate\\Container\\BoundMethod',
            'type' => '::',
            'args' => [            
                0 => Illuminate\Foundation\Application#7(...),
                1 => [                
                    0 => App\Console\Commands\TEMPEasyPostApi(...),
                    1 => 'handle',
                ],
                2 => [],,
                3 => null,
            ],
        ],
        17 => [        
            'file' => 'vendor/laravel/framework/src/Illuminate/Console/Command.php',
            'line' => 211,
            'function' => 'call',
            'class' => 'Illuminate\\Container\\Container',
            'type' => '->',
            'args' => [            
                0 => [                
                    0 => App\Console\Commands\TEMPEasyPostApi(...),
                    1 => 'handle',
                ],
            ],
        ],
        18 => [        
            'file' => 'vendor/symfony/console/Command/Command.php',
            'line' => 326,
            'function' => 'execute',
            'class' => 'Illuminate\\Console\\Command',
            'type' => '->',
            'args' => [            
                0 => Symfony\Component\Console\Input\ArgvInput#8
                (
                    [*:definition] => Symfony\Component\Console\Input\InputDefinition(...)
                    [*:stream] => null
                    [*:options] => [...],
                    [*:arguments] => [...],
                    [*:interactive] => true
                    [Symfony\Component\Console\Input\ArgvInput:tokens] => [...],
                    [Symfony\Component\Console\Input\ArgvInput:parsed] => [...],
                ),
                1 => Illuminate\Console\OutputStyle#9
                (
                    [Symfony\Component\Console\Style\OutputStyle:output] => Symfony\Component\Console\Output\ConsoleOutput(...)
                    [Symfony\Component\Console\Style\SymfonyStyle:input] => Symfony\Component\Console\Input\ArgvInput#8(...)
                    [Symfony\Component\Console\Style\SymfonyStyle:output] => Symfony\Component\Console\Output\ConsoleOutput(...)
                    [Symfony\Component\Console\Style\SymfonyStyle:lineLength] => 120
                    [Symfony\Component\Console\Style\SymfonyStyle:bufferedOutput] => Symfony\Component\Console\Output\TrimmedBufferOutput(...)
                    [Illuminate\Console\OutputStyle:output] => Symfony\Component\Console\Output\ConsoleOutput(...)
                    [*:newLinesWritten] => 1
                    [*:newLineWritten] => false
                ),
            ],
        ],
        19 => [        
            'file' => 'vendor/laravel/framework/src/Illuminate/Console/Command.php',
            'line' => 181,
            'function' => 'run',
            'class' => 'Symfony\\Component\\Console\\Command\\Command',
            'type' => '->',
            'args' => [            
                0 => Symfony\Component\Console\Input\ArgvInput#8(...),
                1 => Illuminate\Console\OutputStyle#9(...),
            ],
        ],
        20 => [        
            'file' => 'vendor/symfony/console/Application.php',
            'line' => 1096,
            'function' => 'run',
            'class' => 'Illuminate\\Console\\Command',
            'type' => '->',
            'args' => [            
                0 => Symfony\Component\Console\Input\ArgvInput#8(...),
                1 => Symfony\Component\Console\Output\ConsoleOutput#10
                (
                    [Symfony\Component\Console\Output\Output:verbosity] => 32
                    [Symfony\Component\Console\Output\Output:formatter] => Symfony\Component\Console\Formatter\OutputFormatter(...)
                    [Symfony\Component\Console\Output\StreamOutput:stream] => {resource}
                    [Symfony\Component\Console\Output\ConsoleOutput:stderr] => Symfony\Component\Console\Output\StreamOutput(...)
                    [Symfony\Component\Console\Output\ConsoleOutput:consoleSectionOutputs] => [...],
                ),
            ],
        ],
        21 => [        
            'file' => 'vendor/symfony/console/Application.php',
            'line' => 324,
            'function' => 'doRunCommand',
            'class' => 'Symfony\\Component\\Console\\Application',
            'type' => '->',
            'args' => [            
                0 => App\Console\Commands\TEMPEasyPostApi#11
                (
                    [Symfony\Component\Console\Command\Command:application] => Illuminate\Console\Application(...)
                    [Symfony\Component\Console\Command\Command:name] => 'easypost:test'
                    [Symfony\Component\Console\Command\Command:processTitle] => null
                    [Symfony\Component\Console\Command\Command:aliases] => [...],
                    [Symfony\Component\Console\Command\Command:definition] => Symfony\Component\Console\Input\InputDefinition(...)
                    [Symfony\Component\Console\Command\Command:hidden] => false
                    [Symfony\Component\Console\Command\Command:help] => ''
                    [Symfony\Component\Console\Command\Command:description] => 'Command description'
                    [Symfony\Component\Console\Command\Command:fullDefinition] => Symfony\Component\Console\Input\InputDefinition(...)
                    [Symfony\Component\Console\Command\Command:ignoreValidationErrors] => false
                    [Symfony\Component\Console\Command\Command:code] => null
                    [Symfony\Component\Console\Command\Command:synopsis] => [...],
                    [Symfony\Component\Console\Command\Command:usages] => [...],
                    [Symfony\Component\Console\Command\Command:helperSet] => Symfony\Component\Console\Helper\HelperSet(...)
                    [*:laravel] => Illuminate\Foundation\Application#7(...)
                    [*:signature] => 'easypost:test'
                    [*:name] => 'easypost:test'
                    [*:description] => 'Command description'
                    [*:help] => null
                    [*:hidden] => false
                    [*:isolated] => false
                    [*:isolatedExitCode] => 0
                    [*:aliases] => null
                    [*:components] => Illuminate\Console\View\Components\Factory(...)
                    [*:input] => Symfony\Component\Console\Input\ArgvInput#8(...)
                    [*:output] => Illuminate\Console\OutputStyle#9(...)
                    [*:verbosity] => 32
                    [*:verbosityMap] => [...],
                    [*:signals] => null
                ),
                1 => Symfony\Component\Console\Input\ArgvInput#8(...),
                2 => Symfony\Component\Console\Output\ConsoleOutput#10(...),
            ],
        ],
        22 => [        
            'file' => 'vendor/symfony/console/Application.php',
            'line' => 175,
            'function' => 'doRun',
            'class' => 'Symfony\\Component\\Console\\Application',
            'type' => '->',
            'args' => [            
                0 => Symfony\Component\Console\Input\ArgvInput#8(...),
                1 => Symfony\Component\Console\Output\ConsoleOutput#10(...),
            ],
        ],
        23 => [        
            'file' => 'vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php',
            'line' => 201,
            'function' => 'run',
            'class' => 'Symfony\\Component\\Console\\Application',
            'type' => '->',
            'args' => [            
                0 => Symfony\Component\Console\Input\ArgvInput#8(...),
                1 => Symfony\Component\Console\Output\ConsoleOutput#10(...),
            ],
        ],
        24 => [        
            'file' => 'artisan',
            'line' => 37,
            'function' => 'handle',
            'class' => 'Illuminate\\Foundation\\Console\\Kernel',
            'type' => '->',
            'args' => [            
                0 => Symfony\Component\Console\Input\ArgvInput#8(...),
                1 => Symfony\Component\Console\Output\ConsoleOutput#10(...),
            ],
        ],
    ]
    [Exception:previous] => GuzzleHttp\Exception\ClientException#3(...)
    [*:response] => Saloon\Http\Response#2(...)
    [*:maxBodyLength] => 200
)

And the connector:

<?php

namespace App\Http\Integrations\EasyPost;


use Saloon\Helpers\OAuth2\OAuthConfig;
use Saloon\Http\Connector;
use Saloon\Traits\OAuth2\ClientCredentialsGrant;
use Saloon\Traits\Plugins\AcceptsJson;
use Saloon\Traits\Plugins\AlwaysThrowOnErrors;
use Saloon\Traits\Plugins\HasTimeout;

class EasyPostConnector extends Connector
{
    use AcceptsJson;
    use HasTimeout;
    use ClientCredentialsGrant;
    use AlwaysThrowOnErrors;

    protected int $connectTimeout = 60;

    protected int $requestTimeout = 120;


    public function __construct(string $clientId, string $clientSecret)
    {
        $this->oauthConfig()->setClientId($clientId);
        $this->oauthConfig()->setClientSecret($clientSecret);
    }

    /**
     * The Base URL of the API
     */
    public function resolveBaseUrl(): string
    {
        return config('services.easypost.base_url');
    }

    /**
     * Default headers for every request
     */
    protected function defaultHeaders(): array
    {
        return [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ];
    }

    /**
     * Default HTTP client options
     */
    protected function defaultConfig(): array
    {
        return [];
    }


    protected function defaultOauthConfig(): OAuthConfig
    {
        return OAuthConfig::make()
            ->setDefaultScopes(['connect/read:jobs', 'connect/submit:jobs', 'connect/read:sending-events'])
            ->setTokenEndpoint('https://auth.acc.connect.easypost.eu/oauth2/token');

    }


}



from saloon.

juse-less avatar juse-less commented on June 2, 2024

Hmm. Very odd.
The cURL command you use will implicitly use POST.
But the error message from Guzzle states that it's also using POST.
And the URLs look identical to me.

I can't really see anything wrong.
Could you maybe remove the default headers in the Connector to test if that changes anything?
It's a long shot, and technically has nothing to do with the error, but worth a shot.

I'm currently on my phone, but can test some stuff a bit later today. 🙂

from saloon.

labomatik avatar labomatik commented on June 2, 2024

Oh yes! removing the header did the trick, strange that the json is causing a 405 MethodNotAllowedException ...

from saloon.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.